我先把 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)。鉴权交给 -infraAuthContextMiddleware(fail_closed)。
配置 common/settings.py SupervisorSettings(pydantic)——唯一配置对象。富类型 llm_models/llm_hooks/prompt/mcpfrom_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 字段)、PlanStepDispatchResult(含卡片元数据 card_type/card_data/bubble_text/trace_id)、UserContextIntentSlot

二、编排层(两种模式,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,内部走 SubAgentProviderbefore_agentuser_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.pycard_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        → 卡片渲染(前端透传)

两点值得你注意的现状

  1. 两个模式并存workflow(确定性 DAG,生产主路径)和 agent(ReAct 自主,简单场景)由 mode 切换,走不同编排实现但共用 sub_provider/llm_hooks/a2ui
  2. DIP Provider 三件套里 promptstools(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 透明。


四、为什么这么设计(要点)

  1. supervisor 不持有安全策略:审核规则在网关侧统一配置,supervisor 业务代码零改动即可跟随策略更新。
  2. fail_reason=stop 的巧妙处:如果网关返回错误码,supervisor 就得写一堆 if 安全错误 分支;伪装成正常响应后,supervisor 的主流程完全无感,符合"极简 + 不重复造轮子"。
  3. 双端覆盖:输入审核(用户原话违规)+ 输出审核(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.pyagent_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(规划节点)

开发前它是这样的:

  1. 计划就是一个"平铺清单":planner 让大模型(LLM)返回一串步骤,每个步骤只有 agent(派给谁)、skill(调啥技能)、message(说啥)。没有"谁先谁后""能不能并行"的概念——步骤之间啥关系全靠 LLM 自己瞎拍脑袋,而且代码根本没校验。

  2. 模板和代码对不上:给 LLM 看的规划说明书(planning.md)里写的是 tool_id{intents} 这种字段,但代码里实际用的字段叫 agentskill等于说明书教一套、代码认另一套,接上就坏

  3. 没有长期记忆:虽然项目里已经写好了"读用户长期记忆"的工具(read_memory),但 planner 压根没用,用户偏好(比如"爱吃素"“行动不便”)完全没进规划。

  4. 回退很傻:大模型万一抽风/挂了,planner 的兜底方案是"一个意图=一步,全丢一个组里",没有排序、没有去重、没有依赖

  5. 重规划会全量重跑:如果某步失败了要重来(reflector 决策 replan),planner 会把所有步骤重新规划一遍,已经成功执行的步骤也跟着重跑——如果是"发消息""下单"这种写操作,就重复副作用了。

  6. 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

三、评审时你重点要问的几个点(潜在坑)

  1. 记忆是"假"的:这次只接 NoOpMemory(空兜底),真实腾讯 AGM 记忆没接。plan 里 Task 3 的记忆注入跑起来 facts 永远是空——这是有意为之(先通链路),但要跟评审方说清楚,别让人误以为个性化已经生效。

  2. user_id 来源取巧:Task 5 从 copilotkit.userId 取,但之前对话里你确认过应走 JWT 入站注入。这里用 copilotkit 可能取不到真实 user_id,需要确认前端到底有没有传这个字段,否则记忆个性化(即使将来接真记忆)也会因 user_id 为空而跳过。

  3. reflect 函数签名假设:Task 6 测试里传了 dispatch_results 字段,但得确认现有 reflector.reflect 真的接收/读取这个字段,否则测试会改不动。

  4. 与 AGM 任务的关系:这份 plan 的"记忆注入"只是上半场,下半场(真实记忆 + 写记忆 MQ 链路)还在另一个未完成任务里——评审时别把它当成"记忆全做完了"。

一句话给评审方:这份设计把 planner 从"平铺傻规划"升级成"带依赖图 + 记忆感知 + 智能回退 + 差集补偿"的规划器,代码层面自洽可测;但真实长期记忆和 user_id 可信来源还需要后续任务补齐,本次先用兜底跑通。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐