分享 / Odoo / 从代码看 Odoo 20.0 的 AI:会话、工具与计费的重构

从代码看 Odoo 20.0 的 AI:会话、工具与计费的重构

Odoo 20 发布后,官方宣传里 AI 占了很大篇幅。营销归营销,我们把 20.0 的源码翻了一遍,对照 19.0,从代码层面看 AI 到底变了什么。先说清楚取材范围:社区版(odoo/odoo)两个大版本里都没有 ai 模块,产品化的 AI 功能几乎全部在企业版(odoo/enterprise)。所以本文的主战场是企业版,社区版只讲框架层的配套变化。

19.0 已经有什么:别以为 AI 是 20 才有的

19.0 的企业版已经有完整的 AI 基础设施,共 16 个 ai_* 模块:ai(基础模块)、ai_fields(AI 计算字段)、ai_server_actions(AI 服务端动作)、ai_documents、ai_knowledge、ai_crm、ai_livechat、ai_website 等。它的架构是:

  • 自带 Key 直连模型厂商:ai/utils/llm_providers.py 里硬编码 PROVIDERS 列表(OpenAI、Google 两家,含各自 embedding 模型与候选 LLM 清单),ai/utils/llm_api_service.py 的 LLMApiService 直接用 requests 调 OpenAI 的 /responses 和 Google 的 generateContent 接口。API Key 来自系统参数 ai.openai_key / ai.google_key(ai/models/res_config_settings.py)或环境变量。
  • 会话就是 Discuss 频道:discuss.channel 扩展了 channel_type="ai_chat"(ai/models/discuss_channel.py),聊天历史直接取频道里的 mail message。
  • Agent + 话题 + 工具:ai.agent 上有 llm_model、response_style、topic_ids;工具(use_in_ai 的服务端动作)挂在 ai.topic 上,工具方法直接写在 ai.agent 模型里(_ai_tool_search、_ai_tool_open_menu_list 等,以检索和打开视图为主)。工具循环在 LLMApiService.request_llm 内部完成,注释原话是 “will call the tools until we are done”。
  • RAG 已就位:pgvector 向量检索(ai/orm/field_vector.py,配合 ai_auto_install 检测 pgvector)、agent sources(ai.agent.source 挂附件与网页)、[SOURCE:210] 风格的引用标注,语音转写(Whisper /audio/transcriptions)和实时转写的 client secrets 也已经在 19.0 出现。此外还有 ai.composer(文本字段的 AI 起草)、ai.prompt_button(记录上的快捷 AI 按钮)、is_ask_ai_agent(数据库级自然语言问答的专用 agent)——今天 20 宣传里的不少能力,19.0 的代码里已经有雏形。

所以准确的说法是:20.0 不是"从无到有",而是把 19.0 这套东西推倒重做了一层。旧架构的局限也很实在:对话状态散落在频道消息里(每次请求都靠重新拼聊天历史),工具循环是请求进程内的同步 while 循环,没有工具确认和挂起恢复。

企业版 20.0:ai_* 模块从 16 个涨到 50 个

数一下 20.0 企业版源码树,ai_* 前缀模块有 50 个,而 19.0 是 16 个。新增的大头是"每个业务应用一个 AI 挂载模块":ai_helpdesk、ai_project、ai_purchase、ai_sale、ai_stock、ai_calendar、ai_mass_mailing、ai_social、ai_esg、ai_product 等,manifest 的 summary 高度一致——“Augment XX with AI Agents”。另一组是 ai_marketing_automation 及其 5 个桥接模块(把 CRM、优惠券、SMS、电商规则喂给营销活动生成器),还有 ai_agentic、ai_agentic_documents、ai_agentic_knowledge、ai_mcp、ai_html_builder、ai_cloud_storage、ai_timesheet_grid 等。19.0 的 ai_app(AI 应用壳)在 20.0 消失,由 ai_agentic 承接——两者的 manifest description 一字不差,但 ai_agentic 额外依赖 base_automation 并引入了 ai.skill 视图。模块名清单本身就是"20 在 AI 上投入了什么"的直接证据。

Provider 层:从自带 Key 直连,到 IAP 网关

这是最主要的一处改动。20.0 的 ai 模块里,llm_providers.py 和 llm_api_service.py 整个删除,取而代之的是 ai/utils/ai_utils.py:

DEFAULT_ODOO_AI_ENDPOINT = 'https://ai.api.odoo.com'

所有 LLM 调用改为 call_odoo_ai() 走 iap_jsonrpc 打到 Odoo 官方 AI 网关,错误类型换成了 InsufficientCreditError,没有余额就发通知。这意味着你的 AI 使用的 token 消耗将由 Odoo 来定价,你的 AI provider 的订阅不能直接支持。

顺带一提,19.0 的 ai.agent 上有 llm_model 选择字段(GPT-4o 到 GPT-5 一串)和 response_style(analytical/balanced/creative,映射到不同 reasoning 档位);20.0 的 ai.agent 把这些字段全删了——模型选择权收归服务端,客户端只剩 enable_think_longer 这类开关(ai.session 字段)。搜索类工具也加了护栏:内部模型(ai. 前缀)与一组敏感模型(res.groups、res.users.settings、base.automation 等)被 AI_MODELS_BLOCKLIST 挡在 AI 可见范围之外(ai/utils/ai_utils.py)。

会话:从 Discuss 频道到 ai.session 状态机

19.0 没有会话模型,对话散在 ai_chat 频道的消息流里。20.0 新增了专门的 ai.session 和 ai.session.event(ai/models/ai_session.py),这不再是一张聊天记录表,而是一台agent 循环状态机:

loop_state = fields.Selection([
    ("ready", "Ready"),
    ("waiting_model", "Waiting for Model"),
    ("waiting_confirmation", "Waiting for Confirmation"),
    ("waiting_answer", "Waiting for Answer"),
    ("waiting_client_result", "Waiting for Client Result"),
    ("waiting_external_result", "Waiting for External Result"),
    ("waiting_child", "Waiting for Child Session"),
], ...)

配合 pending_tool_call(等待客户端执行的工具)、resume_token(挂起后恢复的令牌)、request_uuid(唯一索引)与 request_webhook_secret,模型上还挂了一组 SQL CHECK 约束,保证"处于等待态就必须保留恢复数据"。每次 LLM 往返记录成一条 ai.session.event,内容是带类型的 JSON parts:文本、工具调用请求、工具结果、thinking 签名(ai/utils/types.py 里的 TextPart / ToolCallPart / ToolResultPart)。

会话本身也不再只属于聊天:每条 ai.session 都带 res_model/res_id(当前正在看的那条业务记录)、state(工具调用之间传递数据的 JSON)、show_agent_steps(把 agent 步骤展示给用户)、turn_notifications(本轮攒着、最终回复后统一弹出的通知),以及 auto_confirm(免确认执行工具)。调用方式也变了:19.0 是本地进程内同步循环;20.0 把补全请求发给 IAP 网关后挂起,由网关回调 /ai/completion_result_ready 继续循环(ai/controllers/thread.py)。这个回调路由 auth="public" 但用 HMAC 签名验证(secrets.compare_digest 比对 request_webhook_secret),而且路由类型用的是新的 type="json2"——和官方"JSON-2 API 成为标准"的说法正好互相印证。工具循环轮数有 request_round_limit 约束兜底,防止死循环烧钱。

工具调用:注册表、命名工具和客户端工具

19.0 的工具方法直接长在 ai.agent 上;20.0 把它们搬进了抽象模型 ai.tool(ai/models/ai_tool.py),工具按名字注册、按 _access_domain 收权限,新增了一批 19.0 没有的"动手类"工具:_ai_tool_create_records、_ai_tool_update_records(带预览确认)、_ai_tool_ask_user_question(向用户追问,会话进入 waiting_answer)、_ai_tool_load_skills,以及子代理工具 _ai_tool_start_session——父会话可以委派 allowed_agent_ids 里的子 agent 开子会话,深度限制 4 层,这是 19.0 完全没有的多代理协作。

工具本体仍是 ir.actions.server(use_in_ai 字段 19.0 就有),但 20.0 给它加了 ai_tool_name 字段:必须匹配 [a-zA-Z0-9_]{1,64},还有部分唯一索引 UNIQUE(ai_tool_name) WHERE use_in_ai。也就是说 20.0 里"给 AI 用的服务端动作"是按名字精确注册、带 schema 校验参数的正式接口,不再是按 ID 捞一把。19.0 的工具全在服务端执行;20.0 出现了客户端工具:有些工具(如打开某个视图、切换看板)由浏览器执行,服务端通过 bus 频道 ai.session/client_tools 下发,执行完带着 resume_token 回来继续循环。危险动作还有确认闸门:工具可以要求用户点头,会话进入 waiting_confirmation,用户可选 confirm_once / auto_confirm / decline(ai/utils/ai_utils.py 里的 UserInputResponse)——“AI 改你数据之前先问你"这个能力,是 20.0 才有的。

从话题到技能:ai.skill

19.0 把"指令 + 工具"的打包叫话题(ai.topic),20.0 改名 ai.skill,语义更清楚了:ai/models/ai_skill.py 里每个 skill 有 instructions 和 tool_ids(指向 use_in_ai 的服务端动作),按有无工具分为 “executable” 或 “guidance” 两类;系统预装的 native skill 带保护,创建、修改、删除都会被拦下。新 agent 默认勾上全部 native skill(ai.agent._default_skill_ids);会话侧则有 enable_tools / enable_skills 开关,模型还能通过 _ai_tool_load_skills 按需拉取技能内容。

网页搜索、抓取与通话

20.0 的 ai.session 上有 enable_web_search 开关,网页搜索本身被实现成一个内置 AI 服务端动作(ai.ir_actions_server_ai_web_search,见 ai/models/ir_actions_server.py)。批量网页抓取也是新东西:ai.web.page(URL 唯一、sha1 内容校验、多 agent source 共享同一份抓取结果)和 ai.web.scraper.batch(状态机 to_submit → submitted → done/failed),抓取任务提交给服务端处理,完成后回调 /ai/url_scraper_result_ready/<batch_uuid>。通话侧新增 discuss.call.history、mail.call.artifact 和 call.transcription.mixin,rtc 控制器支持按时间段转写通话录音——19.0 只有单段音频转写,没有这套"通话记录 + 分段转写 + 摘要"的组合。

MCP:Odoo 作为工具服务器

新增的 ai_mcp 模块把 Odoo 变成一台 MCP 服务器:/mcp 路由(auth="bearer", bearer_scope="mcp")处理 initialize / ping / tools/list / tools/call 四个方法,把 use_in_ai 的服务端动作暴露成 MCP 工具,外部 Claude 之类的客户端用带 mcp scope 的 API Key 直连,请求分发在 ai.mcp.request.dispatcher 抽象模型里完成,且有独立的访问控制测试(ai_mcp/tests/test_mcp_access.py)。这依赖社区版 20.0 的一个框架变化,见下一节。

社区版框架层的配套变化

社区版两个大版本都没有 AI 模块,但 20.0 有几处框架级改动是上述功能的地基:

  1. HTTP 层重构:odoo/http.py 从单文件拆成 odoo/http/ 包(dispatcher.py、routing_map.py、session.py、stream.py 等)。
  2. bearer 认证支持 scope:19.0 就有 auth="bearer",但 ir_http.py 里写死 scope='rpc';20.0 要求路由显式声明 bearer_scope 并按它校验(odoo/addons/base/models/ir_http.py,scope=routing['bearer_scope'])。没有这个,ai_mcp 的 scope='mcp' 无从实现。
  3. ORM 重构:odoo/models.py 等拆成 odoo/orm/ 包;SQL 约束从 _sql_constraints 字典改为 models.Constraint / models.UniqueIndex 类(odoo/orm/table_objects.py)。20.0 企业版 ai 模块里有 9 处用的是新写法。
  4. 安全声明文件改名:ir.model.access.csv 改为 ir.access.csv(20.0 的企业版 ai 模块 manifest 也同步改了)。
  5. 仓库顶层新增 skills/ 目录:内置 odoo-guidelines、odoo-review、odoo-security、odoo-web-guidelines 四份文档——这是官方给 AI 编码代理准备的"开发技能”,Odoo 自身的开发流程也开始 AI 化了。

对 19.0 用户意味着什么:迁移与注意点

  • 计费模式变了,这是最大的坑。19.0 自备 OpenAI/Google Key,成本直接付给厂商;20.0 走 IAP 信用点,预付给 Odoo,连实时转写都有单独的用量上报接口(/ai/transcription/report_realtime_session_usage,带 iap_transaction_token)。升级前先评估用量和成本结构,并且确认你的区域/部署形态能用 ai.api.odoo.com(自建内网部署还要考虑数据出域合规——对话和上下文会发到 Odoo 官方网关)。
  • 二开接口基本全变。如果 19.0 上你们扩展过 ai.topic、调过 LLMApiService、或者在 ai.agent 上加过 _ai_tool_*,这些类和文件在 20.0 里已经不存在:ai.topic → ai.skill(还分"指导型"和"可执行型",内置 native skill 禁止改删),工具方法 → ai.tool 注册表,频道即会话 → ai.session。按 19.0 接口写的集成代码要重写。好消息是工具的载体没变,仍然是 ir.actions.server——你们 19.0 上写过的 AI 服务端动作,升级后补一个 ai_tool_name 就能挂回工具面板。
  • 社区版升级注意:导入路径受 odoo/http/、odoo/orm/ 拆包影响(顶层 from odoo.http import route 这类常用导入仍有兼容导出,但深层路径要核对);自定义模块的 ir.model.access.csv 要改名为 ir.access.csv;_sql_constraints 建议迁到 models.Constraint。
  • pgvector 依赖不变:ai_fields、embedding 这套 19.0 已有,20.0 继续依赖 pgvector,数据库侧没有新的运维负担。
  • 没升级的 19.0 用户:19.0 的 AI 能用但天花板明显——没有工具确认、没有子代理、没有 MCP、没有通话摘要,且 Key 直连模式下没有官方网关侧的模型路由。如果 AI 是你们选型的重点,20.0 值得认真评估。