同一个任务,三种 Agent 写法
前面三篇分别带你写了 Raw API、LangGraph 和 Agent SDK。这里不再把它们当成三个孤立的框架介绍,而是把同一个小任务放在一起看:
用户问“公司 VPN 怎么申请?顺便帮我看看我上周提交的工单到哪一步了”。
这个请求包含两件事:先查知识库,再查工单。查知识库是只读操作,查工单也不会产生副作用。稍后再把“创建工单”加入流程,观察需要人工确认时,三种写法分别要增加什么。
你应该先把前文的三个版本都跑通,再阅读本篇。比较时不要只看代码行数,最好在每个版本中使用相同的模型、工具返回值和测试问题。
先准备一张对照表
在自己的笔记中建一张表,每完成一个版本就填一列:
| 观察项 | Raw API | LangGraph | Agent SDK |
|---|---|---|---|
| 对话历史放在哪里 | |||
| 模型决定下一步的代码 | |||
| 工具调用由谁执行 | |||
| 工具结果如何回传 | |||
| 没有工具调用时如何结束 | |||
| 失败后从哪里重试 | |||
| 人工确认暂停在哪里 | |||
| 能看到哪些运行轨迹 |
空着的格子不是问题,先写完代码再回填,反而更容易发现“框架帮你做了什么”。
第一步:把同一个任务缩小
先只保留“查询 VPN 申请流程”这一半。知识库工具可以返回一条固定记录:申请入口、审批人和文档更新时间。这样做是为了排除网络和数据差异,让三种实现只比较控制流。
在三个版本中都准备同样的工具契约:
search_knowledge(query: str):只读,返回最多三条相关片段和来源。get_ticket(ticket_id: str):只读,返回工单状态和最近更新时间。create_ticket(title: str, detail: str):有副作用,后面再加入。
先运行一个不需要工具的问题,例如“你好”,再运行“VPN 怎么申请?”。记录模型是否调用工具、调用了几次、最终答案是否引用了工具结果。
在 Raw API 版本里找三处边界
打开 裸 API 手写 Agent,找到下面三段,而不是重新抄一份完整代码:
messages初始化的位置:确认 system、user 和历史消息的顺序。if not msg.tool_calls的结束判断:这是“模型说完了”的唯一出口。run_tool(...)和追加role: tool消息的位置:这是程序接管执行权的边界。
把天气工具替换成 search_knowledge 后,先只支持一个工具。确认你能回答:
- 模型返回的工具名和参数在哪里读出来?
- 工具报错时,哪一条消息让模型知道发生了错误?
- 如果模型连续调用工具,最大轮数由哪一行限制?
在 LangGraph 版本里找三类节点
打开 LangGraph,把裸循环拆成三个最小部分:
model节点:把 state 交给模型,并把 assistant 消息写回 state。tools节点:读取最后一条消息中的调用,执行工具,再写入工具结果。- 条件边:检查最后一条消息有没有工具调用,有就去
tools,没有就结束。
先不要加入规划节点。运行同一问题后,打印每个节点进入和离开的时间。你应该能看到“model → tools → model → END”,这条路径和 Raw API 的两轮循环是一一对应的。
在 Agent SDK 版本里找三个封装点
打开 Agent SDK,把以下概念对应到你刚才手写的代码:
Agent的指令和工具列表,对应 Raw API 的 system prompt 与tools。Runner或运行入口,对应 Raw API 的循环和结束判断。- SDK 的事件、session 或 tracing,对应你手写的日志和
messages管理。
运行同一问题,先只打开一个只读工具。观察 SDK 是否自动完成工具调用循环,并确认你仍然能在工具函数入口处做参数校验。封装减少了样板代码,但工具的安全边界仍然在你的函数里。
第二步:加入第二个工具,观察分支在哪里出现
现在把“顺便查我的工单”加入用户问题。为了让例子可复现,可以先把用户身份写成固定的 user_id = "u-1001",不要一开始就接真实登录系统。
分别运行下面三种输入:
- “VPN 怎么申请?”
- “我的工单 INC-1042 到哪一步了?”
- “VPN 怎么申请,并查一下 INC-1042。”
重点观察第三种输入:模型可能一次返回两个工具调用,也可能分两轮调用。你的实现都应该能正确处理,不能假定每轮只有一个调用。
比较消息和状态的变化
在 Raw API 中,给每一轮打印 len(messages)、工具名和 finish_reason。在 LangGraph 中,打印 state 中 messages 的长度和节点名称。在 Agent SDK 中,打印每个 tool 事件的名称和运行 ID。
将三份日志并排比较,回答:
- 哪一种实现最容易看出“先查知识还是先查工单”?
- 哪一种实现能明确记录两个工具调用属于同一个请求?
- 如果工具结果很长,在哪里做裁剪最自然?
如果你发现三种实现的调用顺序不同,不要马上认为其中一个错了。先检查 system prompt 是否完全一致,再决定是否需要在代码侧固定顺序。需要严格先后关系时,不能只依赖模型“自觉按顺序调用”。
第三步:故意让工具失败
把 get_ticket 改成在工单号为 INC-9999 时返回超时或业务错误。三种实现都运行:
“请查询 INC-9999 的状态。”不要修改模型提示词,先观察原始行为,再逐步加保护。
Raw API:你要显式写出错误协议
在 safe_run_tool 外层增加一个统一错误结构,例如包含 ok、error_code 和用户可理解的 message。然后把这份结构作为 role: tool 的内容回传。
检查两件事:
- 可恢复错误是否只重试一次,避免循环调用。
- 不可恢复错误是否直接结束并说明原因,而不是让模型继续猜。
这里的判断完全由你的代码负责。模型只能根据错误消息提出下一步建议,不能替你决定是否允许再次执行有副作用的工具。
LangGraph:把失败变成一条可见的边
在工具节点返回错误状态后,增加一个条件判断:可恢复错误去 retry,权限错误去 ask_user,其他错误去 END。先只实现一次重试,运行后查看图的路径。
你会看到图的优点不是“自动重试”,而是“重试发生在哪个节点、最多几次、失败后去哪”都能写在图上。代价是 state 需要增加错误码和重试计数,边也会变多。
Agent SDK:确认默认重试和自定义护栏
查你使用的 SDK 版本文档,确认 runner 对工具异常的默认行为:是直接抛出、返回错误事件,还是自动重试。不要凭印象填写。
在工具函数内部保留业务校验,再在 SDK 的 guardrail、middleware 或事件回调位置限制重试次数。运行一次失败任务,记录 SDK 的默认行为和你增加的行为分别是什么。
第四步:在创建工单前暂停
现在加入有副作用的 create_ticket。用户说“帮我创建一个 VPN 权限申请工单”时,三种实现都必须在真正写入前等待人工确认。
先画出预期路径:
理解请求 → 生成工单草稿 → 展示标题和内容 → 人工批准 → 创建工单 → 返回编号Raw API:自己维护暂停状态
不要在一个函数里直接 input("确认吗?") 就结束。把“待确认动作”保存为结构化数据,包括用户、工具名、参数、风险说明和过期时间。下一次请求带着确认结果回来时,再由代码验证确认人和参数是否一致。
测试时故意修改确认后的标题,确认代码会拒绝执行。这个测试能证明安全边界在代码侧,而不是依赖模型记住原始参数。
LangGraph:把人工确认做成节点或中断点
把确认放在 create_ticket 之前,使用框架提供的 checkpoint 或中断机制保存 state。重启进程后,用同一个任务 ID 恢复,确认“待执行工具”和“审批结果”仍然存在。
再测试重复点击批准。正确结果应该是工单只创建一次,第二次点击得到“已处理”的状态,而不是再次调用工具。
Agent SDK:区分确认机制和普通对话
查 SDK 是否提供 human-in-the-loop、approval 或 tool hook。若没有现成能力,就在工具调用真正执行前接入自己的审批服务,不要把确认写进 prompt。
记录 SDK 事件中的调用 ID,并把审批记录和调用 ID 绑定。这样即使 SDK 自动管理了部分 session,你仍然能回答“谁批准了哪一次创建操作”。
第五步:用证据做选型,而不是背优缺点
完成前面的四个步骤后,按下面顺序做决定。
先看流程复杂度
- 只有一个模型、少量只读工具,且失败后直接结束:优先保留 Raw API。
- 出现多个明确分支、循环、暂停和恢复:把流程控制迁移到 LangGraph 一类显式图编排。
- 单个 Agent 需要快速接入工具、session、handoff 和 tracing,流程分支不多:优先评估 Agent SDK。
再看你需要控制什么
- 需要完全掌控请求格式、token 和工具结果:检查 Raw API 版本是否更容易审计。
- 需要查看每个节点的输入输出并从中断处恢复:检查 LangGraph 的 state 和 checkpoint 是否满足要求。
- 需要统一的 Agent 定义、运行事件和多 Agent 交接:检查 SDK 的抽象是否覆盖,而不是自己再包一层。
最后用同一组指标复核
至少记录十条固定问题的以下数据:成功率、工具调用次数、平均延迟、失败后的重试次数、输入输出 token、人工确认是否越过执行边界、调试一条失败轨迹所需时间。
如果一个方案只是代码更短,但无法回答“哪一步最慢、谁批准了副作用操作”,它就不适合直接进入生产。反过来,图和 SDK 增加的配置如果没有带来可观测性或恢复能力,也不必为了“用了框架”而引入。
一个实际的判断练习
假设你的项目当前只有 search_knowledge 和 get_ticket 两个只读工具,平均每次只调用一个工具,没有人工确认,也不需要断点续跑。先用 Raw API 写到可测试,再评估是否真的需要迁移。
半年后需求变成:查询后要生成工单草稿,敏感工单必须审批,审批可能隔天完成,失败要从检索节点重试,还要让审计人员查看完整路径。此时把“迁移到图编排”作为候选方案,并用上面的指标验证,而不是只因为团队听说 LangGraph 很流行。
如果团队已经统一使用某家模型生态,且主要诉求是快速接入 tracing、session 和 handoff,可以先用对应 Agent SDK,再把跨 Agent 的长流程放到外层工作流中。边界要写在架构文档里:SDK 管一个 Agent 的运行,工作流引擎管业务步骤和审批。
本节完成标准
读完本篇,你应该能拿着自己的三份代码回答:
- 同一条用户请求在三种实现里分别经过哪些步骤?
- 哪些状态是框架自动保存的,哪些仍由业务代码负责?
- 工具失败、重复批准和进程重启时,三种实现各自从哪里恢复?
- 你的项目下一阶段最需要的是更少样板代码,还是更强流程控制?
答不出来时,回到对应文章重新运行一个最小问题,打印消息、state 或事件,再继续比较。不要靠阅读代码的印象做结论。