第四十八章 AI 集成(内部模块精读)

篇别:第十六篇 AI 工程集成

本章学习目标

  • 掌握本仓库 ai 模块目录拓扑、模型依赖、定时任务策略ACL 边界
  • 能复述 RAG 全链路ai.agent.source → 附件分块 → ai.embedding + pgvector_get_similar_chunks_build_rag_context
  • 区分 AI 问答WebSocket三类角色(模型 token 流、信令、Bus 新消息)与 SSE单向流式;对照 内置 Discuss AI(RPC + message_post + Bus)与 自研打字机(SSE/WS);掌握 语音浏览器 Realtime WebSocket文本问答 的差异。
  • 理解 LLMApiService.request_llm工具调用循环系统参数限流PREPROMPTS / _ai_serialize_fields_data提示词行为 的约束。
  • 能编写 数据分析向 Prompt,并与 第四十一章 安全原则一致。

导读:与第四十一章、源码的关系

第四十一章产品能力与安全观;本章 按目录读代码,便于 二开、排障与 Code Review。正文默认源码根为:

appstore/odoo_blog/odoo19_devbook/ai/

说明:企业版完整栈中,ir.actions.server 的 AI 状态 等可能依赖 ai_fields 等附加模块;本教材副本中 ir_actions_server.py 仍保留对 ai_fields 的 import,若你只在独立仓库看 ai,需以 实际可安装依赖 为准。用户向功能说明见 AI(User Docs)


48.0 ai 模块拓扑速览

48.0.1 目录职责表

路径 职责
models/ 业务模型与继承:ai.agentai.agent.sourceai.embeddingai.topicai.composerdiscuss.channel / mail_* 补丁ir.actions.server(AI 动作)、ir.attachment(分块/抽取)、base_ai_* 上下文序列化)等。
controllers/ main.py/ai/generate_response 等 Discuss 入口;agent.py/ai/transcription/session 语音会话票据。
utils/ llm_api_service.py:HTTP 调用、request_llm 工具循环llm_providers.py:OpenAI / Google / Qwen 与嵌入模型映射;html_extractor.py:URL 正文抽取;ai_logging.py:调用日志会话。
data/ Cron(见下)、ai_topic_data.xmlai_agent_data.xmlai_composer_data.xml 等演示与默认数据。
static/src/ 后端 Web 资源:Discuss 补丁、ask_ai_buttonvad_audio_recorder.js、HTML 编辑器插件、agent_add_source_dialog 等。
orm/field_vector.py Vector 字段类型及 向量索引 声明(与 PostgreSQL + pgvector 配合)。
security/ir.model.access.csv ai.agent / ai.embedding / ai.topic普通用户只读、系统用户全权 等规则。
tests/ 工具调用、Gemini 集成、Discuss 频道、权限等 单元/集成测试(阅读测试可快速理解契约)。

48.0.2 依赖与资产

  • __manifest__.pydepends: ['mail'];向 web.assets_backendmail.assets_publicim_livechat.assets_embed_core注入补丁,使 Discuss / Livechat / Portal 均能触达 AI 入口。
  • 许可 OEEL-1:与 企业版 分发一致;教学副本 勿当作社区版默认可用能力 对外承诺。

48.0.3 本节练习

  1. 简答:为何 ai 必须依赖 mail 才能形成闭环?
  2. 实操:在源码中 全文搜索 _trigger(),列出 与嵌入/抓源 相关的 cron xmlid

参考答案提示:1. 回复落点message_post / Discuss 线程,无 mail 则 无标准交互面


48.1 内部接入方式(模型、Composer、频道)

48.1.1 核心模型分工

模型 要点
ai.agent LLM 模型系统提示topic_idsai.topicsources_idsresponse_style → temperature;编排 _generate_response(系统消息、RAG、LLMApiService.request_llm、工具)。
ai.topic 主题说明 instructions + tool_idsir.actions.serveruse_in_ai);源码 TODO 注明未来可能 与 AI 型 server action 合并
ai.agent.source binary / url 源、status / is_active / error_details;创建时 绑定附件_trigger 对应 cron(见上一版章内代码引用)。
ai.embedding 分块文本 + embedding_vector余弦距离检索_cron_generate_embedding 批量补向量并 回写 source 状态
ai.composer 界面入口 → Agent 映射interface_key(如 systray_ai_buttonchatter_ai_buttonmail_composer)+ focused_models + default_prompt + available_prompts

48.1.2 Discuss 频道:ai_chat 与上下文 JSON

discuss.channel 扩展 频道类型 ai_chat,并增加 ai_env_context(JSON)与 ai_agent_id

  • ai_agent_id 使用 groups=fields.NO_ACCESS防止成员篡改 代理绑定导致 安全问题(注释写明 garbage collection 等风险)。
  • create_ai_draft_channel:按 interface_key + 可选 focused_modelsai.composer,再 ai_agent._create_ai_chat_channel;把 default_prompt + record._ai_initialise_context(...) 写入 ai_env_context,供后续对话使用。

48.1.3 定时任务:仅 _trigger,不设高频 interval

data/ir_cron.xml 中两条 cron 的 interval_number=9999interval_type=months几乎不会按时间自跑;实际由业务代码 _trigger() 触发(embeddingURL 处理)。升级或排查时 勿误以为「cron 坏了」——要查 是否被 triggerworker 是否处理队列

48.1.4 访问控制(节选)

普通用户ai.agent / ai.topic / ai.embedding / ai.composer / ai.prompt.button / ai.agent.source 多为 只读或无写权限增删改 多在 base.group_system。对接 Portal / Livechat 时,仍需记录规则与消息 ACL 防止 跨租户读向量或附件(详见 security/ir.model.access.csv)。

48.1.5 截图占位

图 48-1 Agent、Topic、Composer、Source 在设置中的关系

48.1.6 本节练习

  1. 简答ai.composerinterface_key 解决什么 路由问题
  2. 判断:Cron 表里 interval 很大 表示 嵌入永远不会执行。( )

参考答案提示:2. 错;_trigger()即时排队


48.2 RAG 与向量层(ai.embedding 精读)

48.2.1 字段与索引

  • embedding_vector = Vector(size=1536)_embedding_vector_idxUSING ivfflat (embedding_vector vector_cosine_ops)):依赖 pgvector;距离算子 <=>_get_similar_chunks 中以 1 - (embedding_vector <=> query) 转为 相似度
  • checksum 关联 ir.attachment:同一文件多 source 可复用向量(与 create_from_attachments 思路一致)。

48.2.2 相似块检索(SQL 核心)

        return self.browse(id_ for id_, *_ in self.env.execute_query(SQL(
                '''
                    SELECT
                        ai_embedding.id,
                        1 - (embedding_vector <=> %s::vector) AS similarity
                    FROM ai_embedding
                    INNER JOIN ir_attachment ON ir_attachment.id = ai_embedding.attachment_id
                    WHERE ir_attachment.checksum = ANY(%s) AND ai_embedding.embedding_model = %s
                    ORDER BY similarity DESC
                    LIMIT %s;
                ''',
                query_embedding, target_checksums, embedding_model, top_n)
            )
        )

解读:仅在 当前 Agent 活动 source 对应的 checksum 集合 内检索,避免 全库扫描embedding_model 必须一致(换模型需 重新嵌入,见 _sync_new_agent_provider 一类逻辑)。

48.2.3 _cron_generate_embedding 两阶段

  1. 建块:对 processing 且有附件 的 source,若 (checksum, model) 尚无块,则 attachment_id._get_attachment_content()_setup_attachment_chunks
  2. 补向量:查找 embedding_vector 为空 的记录,批量调用 get_embedding,失败打 has_embedding_generation_failed;配合 ir.cron._commit_progress 可中断的长任务。最后 source._update_source_status

48.2.4 自动清理

_gc_embeddingsAutovacuum 删除 已无任何 Agent source 引用 的 chunk,防止 孤儿向量 占空间。

48.2.5 本节练习

  1. 简答:为何检索必须带 embedding_model 条件?
  2. 实操:画 source.createindexed状态机processing / failed / indexed)。

参考答案提示:1. 不同模型向量空间不可比,混用会导致 语义检索无效


48.3 问答交互:JSON-RPC、Bus、WebSocket 与 SSE

48.3.1 Discuss 路径:一次 RPC,不是「模型流」协议

控制器 AIController.generate_responseauth="public" + add_guest_to_context,供 Livechat 访客):

    @http.route(["/ai/generate_response"], type="jsonrpc", auth="public")
    @add_guest_to_context
    def generate_response(self, mail_message_id, channel_id, current_view_info=None, ai_session_identifier=None):
        channel = self._get_ai_channel_from_id(channel_id)
        if not channel:
            raise NotFound()
        message = self._get_message_with_access(mail_message_id)
        if message:
            channel.sudo().ai_agent_id.with_context(current_view_info=current_view_info, ai_session_identifier=ai_session_identifier)._generate_response_for_channel(message, channel)

链路

  1. JSON-RPC 一次请求触发生成;HTTP 在该次调用完成后 释放本路由不承载 SSE/WS 的 token 流
  2. _generate_response_for_channelLLMApiService.request_llm(服务端 阻塞至 得到完整回复或工具循环结束)→ _post_ai_responsemessage_post
  3. 浏览器 「像聊天一样更新」:依赖 Discuss / mail 实时通道(常见底层为 WebSocket Bus长轮询)——推送的是 mail.message 事件不是 提供商返回的 delta 逐字流

同类路由/ai/post_error_message/ai/close_ai_chat(关闭 ai_chat 频道时可能 unlink)。


48.3.2 WebSocket 在 AI 问答中的用法(通用 + Odoo 对照)

WebSocket(WS)全双工 长连接,在 AI 问答 类产品里常见 三类角色

角色 典型用途 载荷形态
A. 模型输出流 浏览器 ↔ BFF/网关;把 LLM 流式响应 切成 WS 帧 推给前端 token / chunk、结束标记、错误
B. 信令与控制 停止生成重试换模型附带上下文 ID JSON 指令与 ACK
C. 应用消息总线 聊天 UI 只认 业务消息(人/机器人各一条气泡) 消息 ID、频道、body

与内置 ai 模块的对应

  • C 类:AI 回复写入 message_post 后,Discuss 通过 Bus(多为 WS) 让客户端 收到新消息 —— 属于 「消息级实时」不是 A 类的 token 流
  • A/B 类:标准 ai 模块未提供「Odoo 对浏览器持续推模型 delta」的 专用 WS 路由;自研时可考虑 Odoo HTTP WebSocket API(随版本而异)独立 BFF,并处理 鉴权、多 worker、CSRF、超时
  • 语音:见 48.3.5,为 浏览器 ↔ 提供商 Realtime 的 WS,与 文本问答 区分理解。

运维:WS 需 心跳/重连nginx 配置 UpgradeConnectionproxy_read_timeout多 worker 时常需 集中式 bus 协调订阅关系。


48.3.3 SSE 在 AI 问答中的用法(通用 + Odoo 对照)

SSE(Server-Sent Events) 使用 常规 HTTP:响应头 Content-Type: text/event-stream,正文按 data: …\n\n 分帧;前端可用 EventSource(多为 GET)或 fetch + ReadableStreamPOST 开流 时常用后者)。方向仅服务端 → 浏览器

典型场景

  1. 流式补全:上游 API stream: true 时,Controller 边读上游边 flush 为 SSE,前端 拼接 实现 打字机
  2. 进度事件RAG 检索中 / 工具执行中event: status,与 最终答案 分离。
  3. 单向推送:用户 很少回传控制指令 时,SSE 比 WS 更轻;若需 频繁双向(打断、语音二进制),优先 WS

与内置 ai 模块Discuss AI 默认无 SSE 字流;自研 SSE 时注意 nginx proxy_buffering off(否则 chunk 被攒批流式失效)、worker 超时长连接期间 ORM 事务不要过大输出须消毒XSS)。

SSE 与 WS(仅论「推文字」):SSE 走 HTTP穿透防火墙通常更容易双向二进制WS


48.3.4 三种机制在 AI 问答中的对比表

机制 方向 常见载荷 内置 Discuss AI 自研「打字机」
JSON-RPC 请求-响应 调用参数/空返回 触发 _generate_response_for_channel 投递 job 再推送
WebSocket(Bus) 多向 新邮件消息 收到 AI 气泡整段或分条 message 兼推状态
WebSocket(直连 LLM) 双向 音频 / Realtime 语音转写(48.3.5) 不经 Odoo
SSE 服务端 → 客户端 token / 进度 未使用 HTTP 侧流式首选

一句话:内置路径 = RPC 触发 + 服务端生成 + message_post +(常为 WS 的)新消息推送SSE 或 WS 传模型字流 = 增强架构,需 单独设计


48.3.5 语音:Odoo 换票 + 浏览器 WebSocket → OpenAI Realtime

AgentControllerPOST /ai/transcription/sessiontype="jsonrpc")返回 短期 sessionVADAudioRecorder

    async setupTranscriptionSession(sessionInfo) {
        if (!VADAudioRecorder.socket) {
            VADAudioRecorder.socket = new WebSocket("wss://api.openai.com/v1/realtime", [
                "realtime",
                // Auth
                "openai-insecure-api-key." + sessionInfo.value,
            ]);

要点:音频 不经 Odoo 中继;这是 本模块中明确的 WebSocket 用法实时语音),与 48.3.2 所述 文本 token 流 区分。合规与 DPA 需覆盖 第三方实时通道


48.3.6 本节练习

  1. 简答auth="public" 的 AI 路由 为何仍可能安全?(提示:消息与频道校验
  2. 判断:Discuss 里 AI 回复 逐字出现 一定表示 使用了 SSE。( )
  3. 简答:SSE 流式场景下,nginx 开启响应缓冲 会导致什么 用户体验问题
  4. 简答Bus WebSocket 推送新消息 与 SSE 推送 token 能否 无设计混用?给出 一种 清晰架构。

参考答案提示:2. 错;多为 动画或多条 message不等于 SSE。3. 延迟批量输出像一次性出字 甚至 超时。4. 不宜混用无状态机;可 SSE 仅草稿流、最终以 message_post 定稿,或 单一通道


48.4 LLM 服务层与工具循环

48.4.1 提供商与模型

utils/llm_providers.pyPROVIDERS:OpenAI / Google / Qwen 及其 LLM 列表默认 embedding 模型get_provider / get_provider_for_embedding_model模型与 HTTP 客户端 间路由。

48.4.2 request_llm:多轮工具直到结束

request_llm 外层 _request_llm_silent

  • 读取 ir.config_parameterai.max_successive_calls(默认 20)、ai.max_tool_calls_per_call(默认 20)。
  • 每一轮 _request_llm_* 若返回 tool_calls,则 按名派发Python 可调用;未知工具 写错误回包 让模型重试;超限注入错误消息
  • 支持 __end_message带 schema 的工具提前终止循环 并附加 结语
        AI_MAX_SUCCESSIVE_CALLS = int(self.env["ir.config_parameter"].sudo()
            .get_param("ai.max_successive_calls", "20"))

        AI_MAX_TOOL_CALLS_PER_CALL = int(self.env["ir.config_parameter"].sudo()
            .get_param("ai.max_tool_calls_per_call", "20"))

运维提示:生产环境若 复杂 Agent + 多工具,需 监控调用次数日志ai_logging),避免 成本与延迟失控

48.4.3 ir.actions.server 的 AI 工具形态(概要)

  • state 扩展 aiAI Actionai_action_promptai_tool_ids
  • use_in_ai / ai_tool_schema:把 server action 暴露为 LLM 可调工具JSON Schema 校验参数,与 utils/tools_schema 配合)。
  • 警告:若工具链上 某 action 带 group_ids,界面会 ai_tool_show_warning——工具执行仍可能跳过部分访问检查须与第四十一章「工具层不得 sudo 扩大权限」一并设计

48.4.4 本节练习

  1. 简答ai.max_tool_calls_per_call 限制的是 什么粒度 的调用?
  2. 实操:在测试库 调低 ai.max_successive_calls2,观察 复杂提问 的行为变化。

参考答案提示:1. 单次 LLM 返回中 并行 tool_calls 处理条数上限


48.5 系统提示与记录上下文(PREPROMPTS_ai_*

48.5.1 PREPROMPTS 字典(可继承覆盖)

ai_agent.py 顶部 PREPROMPTS(节选语义):

  • tools:仅当 用户明确最合适 时使用工具;日期 可用「今天」推算
  • restrict_to_sources除问候外 必须 仅用上下文与历史禁止编造;无上下文时 固定句式 提示未提供源。
  • context / unaccessible_references引用段落不可见源 的处理规则。

restrict_to_sources 在 Agent 勾选 restrict_to_sources 时追加到系统消息,与 48.6 数据分析 Prompt「不得臆造数字」 一致。

48.5.2 记录 JSON:_ai_serialize_fields_data**

models/models.pybase 继承:

  • AccessError 字段直接跳过——不进入 AI 上下文
  • many2many / one2many 超过 50 条省略该字段,防 提示词爆炸
  • binary 跳过name / display_name 类字段截断 减轻 注入面
  • mail_composer 场景下把 整记录 JSON 塞进 初始 context

48.5.3 URL 抓取:HTMLExtractor

utils/html_extractor.py排除 script/style/nav/footer 等 XPath,降低噪声;失败时 ai.agent.sourcefailed + error_details。与 create_from_urls 仅管理员 组合,控制 SSRF 风险面

48.5.4 本节练习

  1. 简答_ai_serialize_fields_data 跳过 relational 超长列表分析类提问 有何副作用?
  2. 判断PREPROMPTS完全替代 记录规则。( )

参考答案提示:2. 错;ACL/规则 才是 硬隔离


48.6 数据分析常用 Prompt(与模块行为对齐)

以下模板与 restrict_to_sources / 工具调用策略 兼容:若 无连接 BI 工具,应要求模型 只解释已给数字禁止编造

48.6.1 自然语言 → Domain(字段白名单)

系统:仅允许字段 [...];输出 JSON {"domain":[...],"explain":"..."}不得 使用未列出字段。

用户:「上月已交货未开票的销售订单,单笔金额 > X。」

48.6.2 read_group / Pivot 摘要

系统:以下仅为 聚合结果,无明细;用 三段:结论、异常、建议(建议须 可映射到 Odoo 菜单动作)。

用户:粘贴 分组键 + 度量

48.6.3 同比 / 环比

系统:仅使用提供的 两期数字;缺一期则 回答无法比较

用户:给出 本期/上期 销售额与订单数。

48.6.4 数据质量

系统:列出 疑似重复(键:partner_id + amount + date)与 建议核对步骤禁止 建议 unlink()

48.6.5 结合 RAG 的政策解读

系统:仅使用 ##Context information;若源未覆盖 渠道商 A,回答 不知道 并列出 缺失信息

用户:「渠道商 A 是否适用附件中的返点条款?」

48.6.6 截图占位

图 48-2 分析类 Prompt 与字段白名单在 Composer/Agent 中的配置

48.6.7 本节练习

  1. 实操:为 sale.order10 字段白名单 + 禁止 account.move
  2. 简答:为何 48.6.2 要求 「建议映射到菜单动作」

参考答案提示:2. 促使输出 可执行、可审计,减少 空话


48.7 前端资源与扩展点(导读级)

  • static/src/discuss/*_patch.js:Discuss composer、thread、message 与 AI 的衔接。
  • ask_ai_button.js / ai_chat_launcher_service.js:顶栏/快捷 Ask AI
  • components/agent_add_source_dialog:添加 Source 的客户端动作 ai_open_sources_dialog(与 ai.agent.source.action_open_sources_dialog 呼应)。
  • HTML 编辑器prompt_plugin、voice transcription嵌入式组件

深入:按 第十四章 资产与 第三十二章 OWL 注册表 单步调试 加载顺序。


本章综合练习

  1. 组件图:从 用户发消息频道出现 AI 回复,标出 RPC、模型方法、request_llmmessage_post、前端更新
  2. 对比SSE 流式 vs message_post 整段 —— 运维、UX、安全两条
  3. 源码题:说明 _get_similar_chunkschecksum = ANY(%s)安全/性能意义
  4. 综合:若把 ai.max_successive_calls 调到 100,列出 两条风险成本/延迟/锁 任选角度)。

本章配套代码:ai/(见本教材目录)。与 第四十一章第三十六章第十四章 交叉阅读。下一章 第四十九章 Odoo 环境部署 介绍 Ubuntu / Windows / macOS 源码环境搭建。