主题
控制面与引擎的耦合契约
本章目标:
- 看清 acdm-backend ↔ 定制 deer-flow 引擎之间双向调用的完整契约
- 分清两套 HTTP 头家族:
X-Auth-User-*(去引擎)与X-ACDM-*(回控制面)- 理解 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:2024 | langgraph_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_sub | threads.create(metadata={"owner_sub": user_sub, ...}) | langgraph_client.py:155-156 |
| 运行时探测 | _is_aegra_runtime() 判断当前是 Aegra 还是 langgraph | langgraph_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-Auth | ACDM_MCP_SERVICE_TOKEN env | 服务间认证(被 mcp_service_auth 中间件校验) |
X-ACDM-User-Id | configurable.user_id | 代表哪个用户(可逆操作 actor) |
X-ACDM-Agent-Name | configurable.agent_name | 哪个 Agent 在调 |
X-ACDM-Thread-Id | configurable.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.create传metadata.owner_sub(langgraph_client.py:156),引擎acdm_auth的on_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_headers用quote(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 原语 BFFdeer-flow/backend/packages/harness/deerflow/tools/project_primitives/client.py— 项目上下文 BFFdeer-flow/backend/packages/harness/deerflow/auth/acdm_auth.py:10-18— owner_sub 强制注入校验acdm-backend/app/main.py:255-268—mcp_service_auth中间件(反向通道认证关口)
Related Pages
| Page | Relationship |
|---|---|
| 鉴权与授权双层体系 | 本章正向头被该章引擎 owner_sub 隔离消费 |
| Aegra 运行时与 LangGraph | 本章 SDK 调的引擎运行时在该章 |
| 知识源 MCP 与 Agent Tools BFF | 本章反向通道在该章从 BFF 路由侧展开 |
| 可逆 DB 操作与存储层 | 本章 X-ACDM-User-Id 喂给该章 via_agent actor |
| BA 专家报告工作流 | 本章 Hybrid 模式 SDK 调用在该章详解 |