Supervisor-Agent — 服务划分与整体技术架构(代码结构)
我先把 src 目录结构和关键文件摸清楚,再给你梳理。
目录结构已清楚。我并行读几个核心文件确认职责边界,避免凭文件名误判。
顶层已清楚。再读几个关键子模块确认职责(agent 编排、agents 出站、provider 三件套、memory、gateway、a2ui、auth、db、tools)。
代码结构已摸清。下面按"入口 → 编排 → 出站 → 支撑"四层给你梳理,每一层标出模块职责和关键文件。
一、入口层(HTTP 服务 + 配置边界)
| 模块 | 文件 | 职责 |
|---|---|---|
| App 工厂 | main.py |
FastAPI 入口。create_agui_app() 是工厂边界——只在此处接收 SupervisorSettings 一次并解构成窄参数;lifespan 里依次初始化 Redis checkpointer、Nacos 热更新、Registry 自注册、4 个 DIP Provider、编排图,并注册 /agui 端点。/healthz 健康检查。 |
| ASGI 中间件 | middleware.py |
LangfuseRootASGI:pure-ASGI 包裹 /agui,开一个 root trace 覆盖整条流式 SSE 响应(避免 BaseHTTPMiddleware 提前退出 span)。鉴权交给 -infra 的 AuthContextMiddleware(fail_closed)。 |
| 配置 | common/settings.py |
SupervisorSettings(pydantic)——唯一配置对象。富类型 llm_models/llm_hooks/prompt/mcp,from_yaml(带 ${ENV} 占位符解析)+ from_env 兼容层。model_by_hook()/default_model() 解析绑定。 |
| 运行时配置 | config/ |
__init__.py 是 Nacos 热更新通道(get/set_loader);provider.py 是配置源抽象 ConfigProvider Protocol(L0 占位未落地,目前 build_config_provider 直接 raise)。 |
| 状态模型 | models.py |
全局类型定义:SupervisorState(LangGraph StateGraph 的 TypedDict,18 字段)、PlanStep、DispatchResult(含卡片元数据 card_type/card_data/bubble_text/trace_id)、UserContext、IntentSlot。 |
二、编排层(两种模式,DIP 分发)
| 模块 | 文件 | 职责 |
|---|---|---|
| 图构建接口 | agents/provider.py |
AgentProvider Protocol + AgentDeps(窄参数,不含 settings)。WorkflowAgentProvider/AgentModeAgentProvider 两实现,build_agent_provider(settings) 按 mode 分发。这是 DIP 高层依赖 Protocol 的体现。 |
| Workflow 模式 | graph.py |
确定性 DAG:ingest→emergency→intent→planner→dispatch_step⟳→reflector→respond 7 节点。dispatch_step 循环直到 plan 走完或 reflector 判 done;reflector 失败则回 planner 重规划(最多 MAX_REPLAN)。respond 渲染卡片 + LLM 改写正文(铁律:正文不复述卡片)。 |
| Agent 模式 | agent/orchestrator.py |
LLM 自主 ReAct 循环(Anthropic “Building Effective Agents”)。领域 Agent 封装成 LangChain @tool,内部走 SubAgentProvider。before_agent 把 user_context 按 thread_id 存入 _USER_CONTEXT_STORE 侧信道(绕过 CopilotKit middleware 限制)。builder.py 提供 build_llm/build_registry/supervisor_agent_card 工厂。 |
三、业务节点层(workflow 的 7 节点实现)
nodes/ 下每个文件对应一个节点:
| 文件 | 职责 |
|---|---|
intent.py |
意图识别,调 llm_intent hook |
planner.py |
生成 PlanStep 列表,调 llm_planner |
dispatcher.py |
单步分发:把 plan[idx] 交给 SubAgentProvider.call()(A2A 出站),结果写入 dispatch_results |
reflector.py |
反思决策(ReflectDecision:done / replan),调 llm_reflector |
emergency.py |
紧急领域短路(不经正常编排,命中直接 respond) |
四、出站 / 支撑层(DIP Provider 四件套 + 记忆 + A2UI)
| 模块 | 文件 | 职责 |
|---|---|---|
| 子 Agent 出站 | agents/sub_provider.py |
SubAgentProvider Protocol + A2aSubAgentProvider 实现。统一 A2A 调用(httpx envelope + 鉴权 Authorization: Bearer + OTel trace 注入 + 契约解析)。call()/call_parallel() 永不 raise,Fail-Safe 返回 DispatchResult(ok=False)。6 个领域 Agent 白名单。 |
| Prompt Provider | prompts/provider.py |
PromptProvider Protocol + LocalPromptProvider/RemotePromptProvider。从 prompts/ 目录或远端加载模板(ADR-016)。目前节点实际直接读 prompts/ 还是走 provider 需确认——provider 已建好但 graph.py 里没见调用。 |
| MCP Provider | tools/provider.py |
ToolSpec + LocalMcpToolProvider/RemoteMcpToolProvider,MCP 工具发现/调用(ADR-018)。tool_runner.py 执行封装。 |
| LLM Hooks | common/llm_hooks.py |
build_llm_hooks(settings) 按 llm_hooks 绑定把每个场景(intent/planner/reflector/respond)包成 LlmHook,统一 build_gateway_client 经 AI Gateway(安全审核在网关层)。 |
| 鉴权 | auth/token_exchange.py |
RFC 8693 委托 token 换发。login_agent() 启动换 supervisor JWT;exchange_for_downstream(agent) 用用户 token + actor_token 换 delegated JWT(RBAC 收敛 scope)。Fail-Safe 透传原 token。 |
| 长期记忆 | memory/ |
factory.py 按 settings 建 BaseMemory(AGM 或 NoOp 降级);compressor.py 压缩;reader.py 读取;mq_producer.py/mq_consumer.py MQ 终态发布/消费;pg_repository.py/base_repository.py/base_model.py/engine.py DB 落库(supervisor 自身不直连业务库,主要归档视角)。 |
| A2UI 渲染 | a2ui/ |
render.py 把 card_type+card_data 渲染成 create_surface/update_components/update_data_model 操作;catalog.py 卡片 schema 目录(resolve_card_type 归一前端 template_id);actions.py 前端 action。 |
| Inbound A2A | gateway/a2a.py |
supervisor 作为被调方的调试桩:/.well-known/agent-card.json + /a2a(收文本→跑图→回 completed)。设计上 supervisor 非 A2A Server,此为超纲调试用。 |
| 应急检索 | common/emergency_retriever.py |
紧急知识检索(供 emergency 节点)。 |
| Nacos 配置 | common/nacos_config.py |
Nacos dataId 读取封装(短小)。 |
五、一张总图
请求 → main.py(/agui, AuthContextMiddleware, LangfuseRootASGI)
│
├─ mode=workflow → agents/provider.WorkflowAgentProvider
│ → graph.py 7 节点
│ ingest→emergency→intent(llm_hooks)
│ →planner(llm_hooks)→dispatch_step⟳(sub_provider→A2A)
│ →reflector(llm_hooks)→respond(llm_hooks+a2ui.render)
│
└─ mode=agent → agent/orchestrator.build_agent_mode_graph
LLM ReAct + @tool(领域Agent) → sub_provider→A2A
│
支撑(DIP Provider 四件套,lifespan 注入)
├─ llm_hooks → AI Gateway(安全审核在网关)
├─ prompts → prompts/ 模板
├─ tools(mcp) → MCP 工具
├─ auth → token_exchange 委托 token
├─ memory → AGM/NoOp + MQ + PG
└─ a2ui → 卡片渲染(前端透传)
两点值得你注意的现状
- 两个模式并存:
workflow(确定性 DAG,生产主路径)和agent(ReAct 自主,简单场景)由mode切换,走不同编排实现但共用sub_provider/llm_hooks/a2ui。 - DIP Provider 三件套里
prompts和tools(mcp)已建好 Protocol+实现,但graph.py节点目前没见调用——意图/规划/反思的 prompt 可能是直接 import 文本,还是已切到PromptProvider?这块和之前聊的"DIP Provider 框架适配计划(Phase 1-4)"直接相关,是正在迁移中的半成品。
需要我深入某一层(比如确认 prompts/tools Provider 到底有没有被节点实际调用,或 sub_provider 出站 envelope 细节)展开吗?
| LLM Hooks | src/common/llm_hooks.py | small/medium/large 三档 → 4 场景 hook(intent/planner/reflector/respond)+ 1 多模态 hook(image_parser);统一经 AI Gateway;安全审核由 AI Gateway 的 Prompt 安全网关在调用链路中统一处理(输入/输出双端),命中时网关返回 finish_reason=stop 的正常 chat completion 响应(content 为拒答文案),supervisor 纯透传,不识别不替换 |
一、LLM Hooks 是什么
supervisor 里所有"让 LLM 干活"的地方,统一封装成 5 个 hook 函数(都在 src/common/llm_hooks.py):
| hook | 干什么 | 对应节点 |
|---|---|---|
llm_intent |
意图识别(含图片描述) | ingest → intent |
llm_planner |
任务规划(生成 PlanStep 列表) | planner |
llm_reflector |
反思决策(done / replan) | reflector |
llm_respond |
整合回复生成(含 5 类降级话术) | respond |
llm_image_parser |
图片内容描述(多模态) | image_parser |
“三档 → 4 场景 + 1 多模态” 的含义:模型按 small/medium/large 分三档算力(比如 intent 用 small、planner 用 medium、respond 用 large),路由到这 5 个 hook。所有 hook 都只经一个出口 build_gateway_client → AI Gateway,不裸连模型(AI 原生红线)。
二、安全审核为什么"下沉"到 AI Gateway
旧设计里 supervisor 自己有 safety_check_input/output 工具,调 LLM 前后各审一次。v5 解耦后这部分彻底移除,改由 AI Gateway 的 Prompt 安全网关在调用链路里统一做输入/输出双端审核。
关键机制:网关命中违规时,不返回错误码,而是伪装成一次正常响应:
返回 finish_reason=stop 的正常 chat completion,content = "抱歉,我无法协助该请求"
supervisor 收到的就是一条"普通 LLM 回复",于是纯透传——不识别、不替换、不设 degradation_type。
三、完整例子(用户发违规内容)
场景:用户在前端输入"怎么伪造门禁卡进入机房"。
① 入站:/agui 收到消息,ingest 节点把文本塞进 state。
② 调 llm_intent(经 AI Gateway):
supervisor → POST AI Gateway → LLM(intent)
↘ Prompt 安全网关(输入审核)命中
AI Gateway 返回:
{
"choices": [{"finish_reason": "stop",
"message": {"content": "抱歉,我无法协助该请求"}}]
}
③ supervisor 侧:llm_intent 看到的是正常响应(finish_reason=stop),完全不知道被网关拦了。它把这段 content 当成语音识别到的"意图文本",继续往下走。
④ intent → planner → respond:整条链路把"抱歉,我无法协助该请求"当成普通文本透传,最终 respond 节点原样输出,前端渲染气泡。
结果:用户看到拒答文案,全程 supervisor 没有任何安全判断代码、不报错误码、不进降级卡分支。安全逻辑对 supervisor 透明。
四、为什么这么设计(要点)
- supervisor 不持有安全策略:审核规则在网关侧统一配置,supervisor 业务代码零改动即可跟随策略更新。
- fail_reason=stop 的巧妙处:如果网关返回错误码,supervisor 就得写一堆
if 安全错误分支;伪装成正常响应后,supervisor 的主流程完全无感,符合"极简 + 不重复造轮子"。 - 双端覆盖:输入审核(用户原话违规)+ 输出审核(LLM 自己生成了违规内容)都由网关兜,supervisor 两侧都不用管。
代价(对应之前聊的"缺口"):因为透传不识别,supervisor 无法区分"用户被拒"和"正常低质回复",所以 bubble_text / Langfuse trace 里看不到"安全拦截"标记——这是 v5 解耦后有意为之的取舍,不是 bug。
需要我对照代码确认 llm_hooks.py 里这 5 个 hook 当前实际是否都走了 build_gateway_client、是否还有残留的安全判断分支吗?
基本可以这样认为,但要分清楚"哪几个是真的假、哪几个是半真半假",不能一刀切说"五个都假"。
先纠正一个事实:是「四件套」,不是五个
DIP Provider 抽象接口是 4 个(ADR-016~019),不是 5 个:
PromptProvider(ADR-016)AgentProvider(ADR-017)McpToolProvider(ADR-018)ConfigProvider(ADR-019)
另外 SubAgentProvider(ADR-020)是 A2A 出站抽象,不算这"四件套"内,但也是 Protocol 抽象。
逐个数,谁是"真用了"、谁是"假用了"
| 抽象 | 实现写好没 | 节点/业务真用上没 | 服务器上"看起来能跑"的本质 |
|---|---|---|---|
AgentProvider |
✅ 写好 | ✅ 真用了 | main.py 里 agent_provider.build(deps) 按 mode 分发,workflow/agent 两图都经它构建。这是唯一真正接线的。 |
SubAgentProvider |
✅ 写好 | ✅ 真用了 | dispatcher.py + orchestrator.py 都走 get_sub_agent_provider().call(),A2A 出站统一。真用了。 |
PromptProvider |
✅ 写好 | ❌ 没用 | 节点裸传 query / respond 硬编码 prompt,prompts/ 目录 5 个 md 是摆设。实现存在但零消费者,服务器跑的是硬编码路径,假的。 |
McpToolProvider |
✅ 写好 | ⚠️ 半假 | Protocol + local/remote 实现都在,但 supervisor 当前没有任何节点实际 call MCP 工具(工具集是空或没接线)。服务器跑不报错,但没真正被业务消费。 |
ConfigProvider |
❌ 没写好 | ❌ 没用 | build_config_provider 直接 raise NotImplementedError,纯占位空壳。服务器靠 from_yaml/from_env 老路径,这层抽象根本不存在运行实例。 |
所以你的判断怎么修正
不能笼统说"五个都没应用",准确说法是:
服务器上"能调试"的真实情况 =
AgentProvider+SubAgentProvider两个是真的 DIP 抽象在跑;PromptProvider/McpToolProvider实现写好了但业务没接(假用);ConfigProvider连实现都没有(纯假,老配置路径在扛)。
也就是说:4 个抽象里只有 2 个真正落地生效,1 个(prompt)和半个(mcp)是"代码存在但没人调"的半成品,1 个(config)是空壳。服务器能跑,是因为老路径(硬编码 prompt、直连 yaml/env、RegistryClient 直接 A2A)在兜底,DIP 框架只是"搭好架子没填肉"。
这恰好就是你那份 plan.md(DIP Provider 框架适配 Phase 1-4)要干的事:把 intent/planner/reflector/respond 的 prompt 迁到 PromptProvider、把 MCP 工具接线、把 ConfigProvider 落地——让 4 个抽象从"写好没接"变成"业务真依赖 Protocol"。
我来先确认这次新提交的 plan 文件内容,再给你讲现状和设计。
读完了这份 plan。下面用大白话给你讲清楚,评审前你需要知道的两件事:开发前啥样、这次设计要干啥。
一、开发前(现状)—— supervisor 的"规划"原来有多简陋
supervisor 是个"总管 Agent",用户一句话进来,它要把任务拆成步骤分给各个子 Agent(比如食堂 Agent、会议 Agent)去干活。负责"拆任务"的那个环节就叫 planner(规划节点)。
开发前它是这样的:
-
计划就是一个"平铺清单":planner 让大模型(LLM)返回一串步骤,每个步骤只有
agent(派给谁)、skill(调啥技能)、message(说啥)。没有"谁先谁后""能不能并行"的概念——步骤之间啥关系全靠 LLM 自己瞎拍脑袋,而且代码根本没校验。 -
模板和代码对不上:给 LLM 看的规划说明书(
planning.md)里写的是tool_id、{intents}这种字段,但代码里实际用的字段叫agent、skill。等于说明书教一套、代码认另一套,接上就坏。 -
没有长期记忆:虽然项目里已经写好了"读用户长期记忆"的工具(
read_memory),但 planner 压根没用,用户偏好(比如"爱吃素"“行动不便”)完全没进规划。 -
回退很傻:大模型万一抽风/挂了,planner 的兜底方案是"一个意图=一步,全丢一个组里",没有排序、没有去重、没有依赖。
-
重规划会全量重跑:如果某步失败了要重来(reflector 决策 replan),planner 会把所有步骤重新规划一遍,已经成功执行的步骤也跟着重跑——如果是"发消息""下单"这种写操作,就重复副作用了。
-
user_id 没进系统:规划要按用户隔离记忆,但状态里连"当前是哪个用户"这个字段都没有。
一句话总结现状:planner 是个"能跑但很粗糙"的规划器——步骤平铺、无依赖、无记忆、回退傻、重跑浪费。
二、这次设计(plan)要干啥 —— 6 大块改造
设计目标就一句话:让规划更聪明(DAG 依赖编排)+ 更个性化(注入长期记忆)+ 更稳(确定性回退)+ 更高效(差集补偿)。
1. 给步骤加上"身份证"和"依赖关系"(DAG)
- 每个步骤新增
step_id(唯一标识,如canteen-agent-0)和depends_on(依赖哪些前置步骤)。 - 这样步骤之间就能画成一张有向无环图(DAG)——谁依赖谁一目了然。
2. 自动算出"并行分组"(拓扑排序)
- 新增一个
_topological_sort函数:根据依赖关系,自动推导出每组步骤该第几轮执行。- 没依赖的 → 第 1 组,可以一起并行跑;
- 依赖 A 的 → 排第 2 组,等 A 跑完再跑。
- 好处:不再靠 LLM 猜"该不该并行",而是代码确定性推导,更可靠。
- 兜底:如果 LLM 返回了"死循环依赖"(A 依赖 B、B 又依赖 A)或引用了不存在的步骤 → 直接降级回退,绝不崩。
3. 注入长期记忆(个性化规划)
- planner 调
read_memory读取用户的历史偏好(车牌号、爱吃什么、行动不便等),拼进规划说明书的{facts}槽位,让 LLM 规划时参考。 - Fail-Safe 设计:记忆不可用 / 没用户 ID / 记忆为空 → 自动退化成"无个性化规划",绝不阻断主流程。
- 注意:这次先用
NoOpMemory(空记忆兜底)接通链路,真实腾讯记忆后续再接(这也呼应了你之前关心的 AGM 任务)。
4. 差集补偿(重规划只补失败的部分)
- 失败时 reflector 会把"哪些步骤成功了 / 哪些失败了"写进状态(
completed_steps/failed_steps)。 - planner 第二轮规划时跳过已成功的步骤,只规划失败的,避免重复执行(尤其是写操作)。
5. 确定性回退增强
- 大模型挂了时的兜底方案升级:按意图优先级排序(吃饭 > 开会 > 出行…)+ 去重 + 跳过已完成步骤,不再是傻乎乎平铺。
6. 模板对齐 + 配置化 + 接线
planning.md重写:字段对齐代码(step_id/agent/skill/message/slots/depends_on),加 few-shot 示例(依赖场景、并行场景、记忆注入场景)。- 新增
PlanConfig配置(最多几步、记忆取几条、最多重规划几次),从 yaml 读取、工厂层解构成窄参数下传(守 DIP 铁律)。 - 接线:
main.py → AgentDeps → graph → planner 节点,把 memory 和配置一路传下去;_ingest节点顺便从前端上下文提取user_id。
三、评审时你重点要问的几个点(潜在坑)
-
记忆是"假"的:这次只接
NoOpMemory(空兜底),真实腾讯 AGM 记忆没接。plan 里 Task 3 的记忆注入跑起来 facts 永远是空——这是有意为之(先通链路),但要跟评审方说清楚,别让人误以为个性化已经生效。 -
user_id 来源取巧:Task 5 从
copilotkit.userId取,但之前对话里你确认过应走 JWT 入站注入。这里用 copilotkit 可能取不到真实 user_id,需要确认前端到底有没有传这个字段,否则记忆个性化(即使将来接真记忆)也会因 user_id 为空而跳过。 -
reflect函数签名假设:Task 6 测试里传了dispatch_results字段,但得确认现有reflector.reflect真的接收/读取这个字段,否则测试会改不动。 -
与 AGM 任务的关系:这份 plan 的"记忆注入"只是上半场,下半场(真实记忆 + 写记忆 MQ 链路)还在另一个未完成任务里——评审时别把它当成"记忆全做完了"。
一句话给评审方:这份设计把 planner 从"平铺傻规划"升级成"带依赖图 + 记忆感知 + 智能回退 + 差集补偿"的规划器,代码层面自洽可测;但真实长期记忆和 user_id 可信来源还需要后续任务补齐,本次先用兜底跑通。
更多推荐



所有评论(0)