Agent 的工作流与模式
Agent 和固定工作流在图里长得一样,都是节点加箭头。区别只有一个问题:下一步往哪走,是谁决定的。
代码里 add_edge 写死的是工作流,模型输出决定的是 Agent。这一句分不清,后面五种工作流模式和五种多智能体模式全会混成一锅。
一、工作流与 Agent 的边界
课程里给的定义很干脆:工作流有预先确定的代码路径,按特定顺序执行;Agent 由 LLM 动态控制流程走向。换成能动手检查的判断:把图里每条边拿出来问一句------这条边是 if/else 或者写死的条件,还是模型这一次的输出?
全是前者,就是工作流。中间有一段边的走向取决于模型判断(分不分类、要不要查资料、任务怎么拆),那一段是 Agent。
| 类型 | 谁定下一步 | 适合什么 |
|---|---|---|
| 工作流 | 代码,步骤预设 | 流程明确的任务:报表生成、工单处理、文档流水线 |
| Agent | LLM,每步现算 | 步骤数取决于输入、边界说不清的问题:调研、改代码、客服排障 |
这不是"高级程度"的排序。工作流能测、能回归、成本能算出来;交给模型决定,等于每一步都多一个"模型可能选错"的失败点。所以我的判断很直接:**能用 if/else 写死的,先写死。**只有当你事前真的说不出要几步时,才值得把控制权交出去。
两者的边界还很软。LangGraph 里图是骨架,图里某个节点内部可以跑一个完整的 create_agent。所以"我们的流程是固定的"和"我们用上了 Agent"完全可以同时为真------只是那句话信息量约等于零,要看的是哪几段交给了模型。
那一段交给模型之后,里面跑的是一个结构固定的循环,叫 ReAct(推理 + 行动):
| 阶段 | 在干什么 |
|---|---|
| 推理 | 看当前消息,决定要不要调工具、调哪个 |
| 行动 | 发出 tool_calls |
| 观察 | 工具结果作为 ToolMessage 回到消息里 |
| 判断 | 还要不要继续:要继续就回到推理,不要就给最终答案 |
循环只有两个出口:模型自己判断可以交付了(自然停止),或者撞上 max_iterations 被强制掐断。后一种是兜底,不是正常路径------如果你经常靠它停,说明任务没拆清楚,或者工具描述写得让模型永远觉得还差一步。
常见坑:把一条固定流程包装成"一个 Agent 加十个工具"。成本翻倍,行为还从可预测变成了可漂移。反过来,把需要判断的地方硬编码成 if/else,就只能处理你当时想到的那几种情况。
撑起这套东西的只有三个概念:State(一张共享的字段表,所有节点读写它)、Node(普通函数,接收状态、返回增量)、Edge(固定边或条件边)。所谓"编译"是运行时组装图并做结构检查(悬空节点、缺入口之类),不是把代码翻译成什么中间产物。
二、五种工作流模式
下面的代码都是片段,省掉了 import 和图对象的初始化,只留决定机制的那几行。
| 模式 | 形状 | 什么时候用 | 关键实现 |
|---|---|---|---|
| 链式 | A → B → C | 每步输入输出清晰、能逐步收窄 | 固定边串起来 |
| 并行化 | 扇出固定几路 → 汇总 | 同一问题的几个独立维度 | 各写各的键;合并键要 reducer |
| 路由 | 先分类 → 走对应分支 | 输入类型分明,处理方式不同 | 结构化输出 + 条件边 |
| 协调者-工作者 | 先规划 → 派 n 个活 → 合成 | 任务数取决于输入(几章、几个源) | Send 动态生成边 |
| 评估器-优化器 | 生成 → 评审 → 不过就改 | 有明确质量门槛 | 循环边 + 终止条件 |
1. 链式
大纲 → 初稿 → 润色,三步三次模型调用,每步只做一件事。
它比一个巨型 prompt 稳,原因不在"分工",在于每一步只看得到必要的那点输入,上下文短、指令少、输出形态好校验。代价是延迟累加:三步串起来,用户等的是三次往返。
真正的坑是错误传播。中间那步生成了跑偏的大纲,润色那步只会把跑偏的内容写得更好看。所以链式成立的前提是每步产物能被下一步的输入格式验证(大纲是列表、初稿有规定的标题)。验证不了就别链,链了也只是把错误加工得更精致。
2. 并行化
同一个问题的几个独立维度同时分析,最后汇总。教材的例子是智能电动自行车的市场、竞品、技术三路分析。
实现在 LangGraph 里就是同一个起点扇出三条固定边,再收回到汇总节点。要注意的是状态合并:每个分支写自己的键(market、competitor、tech)就没事;多个分支要往同一个键里追加,必须给它配 reducer。
python
from typing import Annotated, TypedDict
import operator
class AnalysisState(TypedDict):
concept: str
market: str
competitor: str
findings: Annotated[list[str], operator.add] # 多路写同一个键,必须给 reducer
builder.add_edge(START, "market")
builder.add_edge(START, "competitor")
builder.add_edge(START, "tech")
operator.add 决定同名键怎么合并:没有 reducer 的键被两路同时更新,会被框架按并发冲突拦下来,而不是"后写的覆盖前面"。
并行省的是墙钟时间,不是 token------三路全都要调模型,费用照付。扇出去十路,就是十路并发打模型,并发上限得自己管。
3. 路由
先判断输入属于哪一类,再走对应分支。智能客服是最典型的场景:售前、售后、技术各有一套提示词和工具。
实现是一个结构化输出加一条条件边,路由函数返回下一个节点名:
python
class Route(BaseModel):
step: Literal["pre_sale", "after_sale", "technical"]
def model_call_router(state: State):
decision = model.with_structured_output(Route).invoke(state["input"])
return {"decision": decision.step}
这段代码本身没什么难度,难的是分类准确率。教材里有 6 条自测用例,跑出来 3 条和用例标注的类别不一致:
- "我购买的产品有质量问题,需要退货" → 分到了售前
- "我的订单已经发货但还没收到" → 分到了售前
- "请问你们的售后服务政策是什么" → 分到了售后(这条标注是售前,但分到售后其实不算错)
前两条是明确的误判,而且都发生在"退货/物流"这类语义上该压过"售前售后"字面词的句子上。
这说明分类错是常态 ,不是异常。所以路由必须配三样东西:一条兜底分支(
other→ 通用处理或转人工)、一份把分类结果和真实结果都记下来的日志、一套按类别算的准确率。少了这三样,路由上线后你只知道"用户体验变差了",不知道差在哪。
顺带一笔:分类器自己也是一次模型调用。路由给流程加了稳定性,也加了一跳延迟和一份成本,别当成免费的。
4. 协调者-工作者
协调者先看任务、定计划,再按计划派人,最后合成结果。写报告是标准例子:协调者生成大纲,大纲几章就派几个工作者各写一章。
它和并行化的分界线只有一条:任务集合是设计时定死,还是运行时算出来的。

左图三条边写死在代码里,右图边数由 assign_workers 的返回值决定。LangGraph 里靠 Send 干这件事:
python
def assign_workers(state: State):
return [Send("llm_call", {"section": s}) for s in state["sections"]]
builder.add_conditional_edges("orchestrator", assign_workers, ["llm_call"])
条件边返回一个 Send 列表,就是运行时动态生成 n 条指向同一节点的边。工作者写完的结果用 Annotated[list, operator.add] 累加,否则 n 份结果互相覆盖。
这里的坑在"n 是模型定的":大纲生成 10 章,就是 10 个并发的模型调用,一次打满。要么限流分批,要么让协调者限制章节数------把上限交给一个刚学会写大纲的模型,等于没有上限。
5. 评估器-优化器
生成,评审,不合格就带着反馈重写,直到过线。文档问答里"检索结果不相关就重写查询再查一遍"(下面第三节那张图)就是它的一种形态。
写这个模式,有两样东西必须先定死,不能留给模型:
- 终止条件:最大轮次或 token 预算。评审不过就永远改,是这类图最容易失控的地方。
- 评审标准 :能用结构化输出的字段表达(
pass: bool、problems: list),别用"你觉得好不好"。
我的观察是评审器和生成器尽量不要用同一个模型、同一套提示词。自己评自己,很容易得到"整体不错,可在细节上进一步完善"这种既不能执行、也不能终止的反馈。
6. 怎么选
一句话版本:步骤固定 → 链式;维度固定 → 并行;输入先分类 → 路由;任务数不定 → 协调者;有质量门槛 → 评估器。
真实项目经常是串起来的:路由先分大类,售后那条分支内部是链式,其中一环用并行拿到三个数据源,最后过一遍评估器。模式是积木,不是选型考卷,一个系统里同时出现三四种很正常。
三、代理式 RAG
普通 RAG 是一条直线:问题 → 检索 → 拼上下文 → 生成。它假设"检索一定有必要,且检索回来的东西一定有用",这两个假设都不牢。
代理式 RAG 在直线两头各加一个判断节点,多出两条回边:

四个节点、两条条件边。第一条用 tools_condition 判断模型要不要调检索工具,它返回 "tools" 或 "__end__":
python
workflow.add_conditional_edges(
"generate_query_or_respond",
tools_condition, # 返回 "tools" 或 "__end__"
{"tools": "retrieve", "__end__": END},
)
第二条用一次结构化评分决定往下走还是往回走:
python
from pydantic import BaseModel
from typing import Literal
class Grade(BaseModel):
binary_score: Literal["yes", "no"]
def grade_documents(state: MessagesState):
score = model.with_structured_output(Grade).invoke(state["messages"]).binary_score
return "generate_answer" if score == "yes" else "rewrite_question"
评分那一步有个容易忽略的性质:文档相关和答案正确是两个维度。检索回来的文档确实在讲这个主题,不等于它能支撑答案。只有前者被检查,后者仍然是幻觉的入口。
教材这份代码还有一个我读的时候不太舒服的地方:生成答案时,原问题取 state["messages"][0],检索结果取 state["messages"][-1]。它靠的是"第一条永远是用户原问题、最后一条永远是检索结果"这个顺序假设。而重写节点是把改写后的问题当成一条新的 user 消息塞回 messages 的------重写一次,messages 里就有两条用户消息了,这个假设靠的只是"恰好没乱"。
我不认为这是教材写错,讲机制时越短越好。但抄进项目就会出事:加一次人工插话、多查一轮、或者换个模型往中间塞条消息,索引就错位了。生产里该显式存字段:original_question、search_query、documents、retry_count,谁都别去猜数组下标。
四、五种多智能体模式
多智能体的第一性问题不是"要几个 Agent",是哪段上下文不该让谁看到。教材把五种模式的区别落在同一组坐标上:谁保持控制权、上下文怎么流动。
| 模式 | 谁保持控制 | 上下文策略 |
|---|---|---|
| Subagents | 主代理(Supervisor) | 把任务派给子代理,只取回结果 |
| Handoffs | 当前阶段的 Agent | 用状态记阶段,切提示词和工具 |
| Skills | 同一个 Agent | 按需加载专业知识和指令 |
| Router | 独立的路由步骤 | 拆开子问题,分派后合成 |
| Custom Workflow | 开发者写的图 | 显式规定每个节点的输入输出 |
1. Subagents
主代理把子代理包装成工具:子代理干完活只返回一段结论,中间的多步推理、工具调用、试错日志全留在子代理自己的窗口里。主代理的上下文始终干净。
两种暴露方式,选型差别不小:
| 方式 | 主代理看到什么 | 扩展一个专家要改什么 |
|---|---|---|
| 每个子代理一个工具 | 一排独立工具,每个的说明和参数 schema 可单独定制 | 新写一个工具函数,加进工具列表 |
一个统一派发工具 task(agent_name=...) |
只有一个 task 工具 |
注册表里加一行,派发逻辑不动 |
子代理少、每个都要精细控制输入输出,用前者;专家多、由不同人维护、要频繁加,用后者。
教材特意标了这个失败模式:子代理调完
create_calendar_event,最终回复只写"已为您安排",没带时间地点参与人。主代理拿着这句空话没法向用户汇报。解法不是改主代理,是在子代理的系统提示词里明确要求"最终回复必须包含执行结果"。子代理的返回值就是它的全部产出,别指望主代理去补。
还有一个常被说错的点:这里的"异步"指主代理起了后台作业、继续和用户对话,不是 Python 的 async/await。await 照样是等结果,只是不阻塞事件循环。
2. Handoffs
行为随阶段切换:一个特殊工具改状态变量,下一轮按新状态换一套提示词和工具。像语音菜单,按 1 进售前、按 2 进技术,你现在在哪一层,决定了能听到什么。
python
@tool
def transfer_to_specialist(runtime) -> Command:
"""转给售后专员"""
return Command(update={
"messages": [ToolMessage(content="转至专业人员处理",
tool_call_id=runtime.tool_call_id)],
"current_step": "specialist", # 下一轮据此换提示词和工具
})
那个 ToolMessage 不能省。模型发起了工具调用,就期待一条带匹配 tool_call_id 的响应来闭合这一轮;缺了它,消息历史就不是一个合法的对话序列。
Handoffs 适合有顺序约束的业务(先拿到保修单号才允许提交退款)。代价是状态机得自己维护,阶段一多,就退化成一张巨大的 if/else------那时候该回到工作流的路由,或者干脆把阶段拆成独立子图。
3. Skills
不拆 Agent,只把知识按需加载。系统提示词里先放技能名和一句说明,模型判断需要时再调工具把完整规则、表结构、示例、模板取下来。这套做法叫渐进式披露。
按体量分档比较实用:
| 技能体量 | 放哪 |
|---|---|
| 小于 1K token | 直接写进系统提示词,配缓存 |
| 1K 到 10K | 按需加载(多数情况) |
| 大于 10K | 分页、搜索或层级展开,别整份塞 |
它和 RAG 思路同源:技能描述是索引,加载全文是检索。区别在检索回来的东西------RAG 取的是给用户答案的素材,Skills 取的是给模型自己看的规则和规范。
坑在"先加载技能再执行"这条约束。它属于流程要求,不能靠模型自觉:得由状态字段、工具内部校验或图路由兜住。提示词里写十遍"必须先加载",也不如工具里查一次
skills_loaded有效。
4. Router
一次分类,分派给一个或多个专家,最后合成答案。知识分散在几个垂直领域(代码库、文档、内部讨论)时最合适,可以并行查多个源。
容易混的是它和 Subagents 的区别:Subagents 是一个持续的代理,维护对话上下文,在多轮里反复决定调谁;Router 通常只是单次分类步骤,派完就不管了。它和工作流里的协调者-工作者其实是同一个东西,只是站在多智能体视角描述了一遍。
想让它有记忆,两条路:把无状态 Router 包装成一个工具,交给带记忆的对话 Agent 调;或者在 state 里存路由历史。后者教材自己也不推荐------多个性格不同的专家来回切,对话感会断。
5. Custom Workflow
标准模式不够用时,回到图:节点可以是纯函数、一次 LLM 调用,或者一个完整的 create_agent。确定性逻辑和 Agent 行为能混着用,多智能体系统也可以整个塞进一个节点。
它是兜底,不是起点。先想清楚是"标准模式表达不了",还是"懒得想清楚业务该分几段"。
五、多智能体的代价
多智能体确实能解决三件事:上下文管理(每次只看相关专业的那一小块)、分布式开发(不同人各维护各的专家)、并行执行(子任务同时跑)。
代价也实打实:模型调用次数翻几倍;信息在"传任务 → 收回结果"之间掉细节,传过去的和想传的经常不是一回事;错误链路变长,最后答案是错的,你还要逐层查是哪个专家、哪次交接出的问题。
所以我的判断是:几个工具加一个好提示词能解决的,别拆多 Agent。拆之前先回答一句------这段上下文,为什么不能让主 Agent 看到?答不上来,拆了只是多几层调用。