Skip to content

控制面与引擎的耦合契约

本章目标:

  1. 看清 acdm-backend ↔ 定制 deer-flow 引擎之间双向调用的完整契约
  2. 分清两套 HTTP 头家族:X-Auth-User-*(去引擎)与 X-ACDM-*(回控制面)
  3. 理解 owner_sub 从注入到校验的闭环,以及 service token 的双向作用

TL;DR

acdm-backend 与引擎是两个独立进程,靠两条 HTTP 通道双向耦合。正向(控制面 → 引擎):langgraph_client.py 用 LangGraph SDK 调 Aegra,注入 X-Auth-User-Id/Email/Name/Role 四头(对齐 Aegra Auth 协议),并在 threads.create 时写 metadata.owner_sub反向(引擎 → 控制面):Agent 工具通过 tools/*/client.py BFF 回调 acdm-backend /agent-tools/*,带 X-ACDM-Service-Auth(service token)+ X-ACDM-User-Id/Agent-Name/Thread-Id 四头。owner_sub 由 acdm-backend 注入、由引擎 acdm_auth.py 校验,形成闭环。

Overview(为什么是"双向 HTTP 契约"而不是共享代码/库)

acdm-backend 是 pip 管理的 FastAPI、引擎是 uv 管理的 vendored LangGraph,两套依赖、两个进程、两种部署节奏。它们之间如果共享 Python 代码会立刻产生依赖冲突和部署耦合。a-cdm 选择把耦合点收敛成两条明确定义的 HTTP 契约:

  • 正向通道让控制面能"把业务任务交给 Agent 跑"(周报、BA 报告、会议聚合);
  • 反向通道让 Agent 能"反过来读写业务数据"(查项目、读 Nextcloud、操作业务对象)而不必懂 SQL/鉴权。

两条通道各有一套身份头,职责严格不同:去引擎的头回答"这个 run 是哪个用户的"(给 owner_sub 隔离用),回控制面的头回答"哪个 Agent 代表哪个用户在哪个 thread 里调我"(给可逆操作 actor 用)。这套契约就是 a-cdm "深度二开但仍保持进程解耦"的关键。

正向通道:acdm-backend → 引擎

acdm-backend/app/services/langgraph_client.py 是 LangGraph SDK 的 thin wrapper:

要素值/机制Source
引擎地址LANGGRAPH_BASE_URL,默认 http://deer-flow-langgraph:2024langgraph_client.py:43-44
客户端单例get_langgraph_client() 复用 SDK httpx 连接池langgraph_client.py:84-94
身份头_auth_headers(user_sub,email,name,is_admin) → 4 个 X-Auth-User-*langgraph_client.py:100-115
owner_subthreads.create(metadata={"owner_sub": user_sub, ...})langgraph_client.py:155-156
运行时探测_is_aegra_runtime() 判断当前是 Aegra 还是 langgraphlanggraph_client.py:66

_auth_headers(langgraph_client.py:100-115)产出的四头与 Caddy forward_auth 注入的完全同名同义(X-Auth-User-Id/Email/Name/Role,email/name 做 quote(..., safe="") URL 编码因 header 只能 ASCII)。这是有意设计:无论身份头是 Caddy 注入还是 acdm-backend SDK 注入,引擎 acdm_auth.authenticate 用同一套解析——这就是"dev/prod 同路径无后门"在服务间调用上的体现。

典型调用(run_weekly_report,langgraph_client.py:119-197):

异常映射:ConnectError → BusinessException(50101)、Timeout → 50102、其他 → 50103,统一进控制面异常契约。BA 报告走 Hybrid 模式(confirm_ba_report_step/regenerate_ba_step/resume_ba_report_from_step,langgraph_client.py:204-366):runs.wait() 从 interrupt 节点恢复,详见 BA 报告章。attach_stream(langgraph_client.py:422)用 SDK join_stream 做 SSE reattach。

反向通道:引擎 → acdm-backend(Agent Tools BFF)

Agent 在跑的时候需要读写业务数据(查项目成员、读 Nextcloud 文件、操作业务对象)。引擎不连 acdm 库、不懂鉴权,所以通过 BFF 回调。deer-flow/backend/packages/harness/deerflow/tools/ 下三套 client:

client调 acdm-backend 路径用途Source
business_objects/client.py/agent-tools/business/{op}业务对象操作tools/business_objects/client.py:52-78
nc_primitives/client.py/agent-tools/nc/*Nextcloud 读写原语tools/nc_primitives/client.py
project_primitives/client.py/agent-tools/project/*项目上下文查询tools/project_primitives/client.py

四个 X-ACDM-* 头(tools/business_objects/client.py:7-10:69-73):

值来源作用
X-ACDM-Service-AuthACDM_MCP_SERVICE_TOKEN env服务间认证(被 mcp_service_auth 中间件校验)
X-ACDM-User-Idconfigurable.user_id代表哪个用户(可逆操作 actor)
X-ACDM-Agent-Nameconfigurable.agent_name哪个 Agent 在调
X-ACDM-Thread-Idconfigurable.thread_id在哪个 thread 里

post_business_op(tools/business_objects/client.py:52-78):ACDM_BACKEND_URL 默认 http://acdm-backend:8002,service token 未设时返结构化 internal error(graceful,client.py:61-64),否则带四头 httpx POST。acdm-backend 侧 mcp_service_auth 中间件(acdm-backend/app/main.py:255-268)校验 X-ACDM-Service-Auth,通过后 X-ACDM-User-Id 等被 reversible_actor_context 用来设 via_agent actor——这就是 AI 写的数据能被追溯/可逆的根源(详见可逆操作章)。

owner_sub 闭环 + service token 双向

这是整套耦合最关键的两个不变量:

  • owner_sub 闭环:acdm-backend threads.createmetadata.owner_sub(langgraph_client.py:156),引擎 acdm_authon_thread_create 不信任客户端值、强制覆盖为 ctx.user.identity(acdm_auth.py:11-12)。即使 acdm-backend 传错也以引擎从 X-Auth-User-Id 解出的为准——双重保险。
  • service token 双向:ACDM_MCP_SERVICE_TOKEN 既是反向通道(引擎 → BFF)的认证凭据,也保护 acdm-backend 的 /mcp/*。它是服务身份不是用户身份——用户身份永远靠 X-ACDM-User-Id 单独传。

Common Pitfalls / 实战 Tips

  • 两套头别混:X-Auth-User-* 是"去引擎"(Aegra Auth 协议),X-ACDM-* 是"回控制面"(BFF)。方向反了会 401。
  • owner_sub 不要在客户端设:传了也会被引擎覆盖(acdm_auth.py:11-12),依赖客户端 owner_sub 是错的。
  • service token 未设的行为不同:引擎侧 post_business_op 返结构化 error(graceful);acdm-backend 侧 /mcp/* 直接拒所有请求。两端都要配 ACDM_MCP_SERVICE_TOKEN
  • email/name 头必须 URL 编码:_auth_headersquote(safe="")(langgraph_client.py:113-114),中文名直接塞 header 会炸。
  • SDK 调用异常已被映射:别在 service 里裸 catch SDK 异常,langgraph_client 已统一成 50101/50102/50103 BusinessException。

References

  • acdm-backend/app/services/langgraph_client.py:43-472 — 正向 SDK wrapper + 身份头(本章主源)
  • deer-flow/backend/packages/harness/deerflow/tools/business_objects/client.py:1-78 — 反向 BFF httpx + 四头
  • deer-flow/backend/packages/harness/deerflow/tools/nc_primitives/client.py — NC 原语 BFF
  • deer-flow/backend/packages/harness/deerflow/tools/project_primitives/client.py — 项目上下文 BFF
  • deer-flow/backend/packages/harness/deerflow/auth/acdm_auth.py:10-18 — owner_sub 强制注入校验
  • acdm-backend/app/main.py:255-268mcp_service_auth 中间件(反向通道认证关口)
PageRelationship
鉴权与授权双层体系本章正向头被该章引擎 owner_sub 隔离消费
Aegra 运行时与 LangGraph本章 SDK 调的引擎运行时在该章
知识源 MCP 与 Agent Tools BFF本章反向通道在该章从 BFF 路由侧展开
可逆 DB 操作与存储层本章 X-ACDM-User-Id 喂给该章 via_agent actor
BA 专家报告工作流本章 Hybrid 模式 SDK 调用在该章详解

公司内部参考 · 由 claude-wiki-gen 基于源码自动生成的二次分析