Appearance
框架实践
这里是一条可以边读边写的教程路线。每篇文章只增加一个关键概念,代码片段保持短小,你需要把它们复制到自己的练习文件中运行,再根据输出做修改。不要把文章当成一个需要一次性复制完成的项目;保留每一步的文件和终端输出,比较它们的变化,才能看见框架到底省掉了什么。
统一案例
核心教程都围绕“企业服务台助手”,只使用三类本地模拟工具:查询知识库、查询当前用户的工单、创建工单。前两类是只读操作,创建工单会产生副作用,必须经过权限检查和人工确认。你可以把模拟数据写成几条字典,等流程跑通后再替换成真实服务。
建议在仓库外准备一个练习目录:
text
framework-playground/
├── step-01-basic-call.py
├── step-02-tool-loop.py
├── step-03-langgraph.py
└── step-04-agent-sdk.py文件名不是硬性要求。关键是每完成一个阶段就留一份可以运行的版本,不要直接覆盖上一阶段。文章中的代码默认从环境变量读取模型地址和密钥;没有密钥时,优先使用文中给出的 Mock 返回值完成控制流练习。
阅读顺序
| 顺序 | 文章 | 你会逐步写什么 | 前置关系 |
|---|---|---|---|
| 1 | 裸 API 手写 Agent | 从一次模型调用开始,加入工具 schema、调用循环、错误结果、日志和流式输出 | 无;先准备 Python 和模型客户端 |
| 2 | LangGraph | 把裸循环拆成 state、model 节点、tool 节点和条件边,再加入分支与人工确认 | 必须先完成裸 API 的消息流 |
| 3 | Agent SDK(官方框架) | 用 Agent、Runner、工具注册和 tracing 重写同一个服务台任务 | 先比较裸 API 与 LangGraph 的控制边界 |
| 4 | 同一个任务,三种 Agent 写法 | 把三份代码放在一起比较,观察控制流、状态、失败恢复和调试信息的差异 | 完成前三篇的最小版本 |
| 5 | 框架实战递进练习 | 在已有代码上增加检索、分支、重试、人工确认,再做一次迁移和选型 | 完成前三篇,建议先读比较篇 |
| 6 | 流式回复中断后的恢复 | 保存已收到的片段,区分重连与续写,处理工具调用中断、幂等和恢复测试 | 至少完成裸 API 的流式输出 |
| 7 | 流式响应的生产级恢复 | 把生成任务、后台 Worker、事件日志和 SSE 订阅拆开,练习重放、多实例并发和故障演练 | 完成流式恢复篇,了解基本的 HTTP/SSE |
如果只想了解概念,可以直接浏览;如果希望真正掌握,至少把第一篇的“直接回答”和“需要工具”两条路径跑通,再进入下一篇。
每一步的验证方式
- 看输入和输出:打印本轮发送的消息类型、工具名称和结构化参数,不要只看最后一句自然语言。
- 看控制流:确认工具由你的代码执行,工具结果以正确的调用标识回传,达到终止条件后才结束。
- 看异常路径:主动让工具返回空结果、参数错误和超时,确认程序不会无限重试或直接崩溃。
- 看安全边界:创建工单前必须停在确认点;拒绝或越权时,真实工具不能被调用。
- 看改写差异:同一个问题分别运行三个版本,比较消息历史、状态快照、节点路径、日志和代码中需要自己维护的部分。
学习清单(问题与详细参考答案)
裸 API 手写 Agent
问题:为什么先写一次普通模型调用,再加入工具调用?
**参考答案:**普通调用能先确认客户端、模型名、环境变量和消息格式都没有问题。工具调用会多出 schema、
tool_calls、参数解析、工具执行和结果回传,任何一层出错都可能被误认为“模型不会用工具”。先让模型稳定返回文本,再逐层增加工具,排错范围更小,也能看清消息历史究竟增加了哪些条目。问题:一轮工具调用循环中,模型、程序和工具分别负责什么?
**参考答案:**模型只负责根据上下文选择“直接回答”或提出结构化工具调用;程序负责校验参数、执行真实函数、限制轮数和把结果写回消息;工具负责访问知识库或业务服务并返回事实。模型不能因为生成了
create_ticket就直接改变数据库,真正的副作用必须经过代码侧的权限和确认检查。问题:工具抛出异常时,为什么要把结构化错误回传给模型?
**参考答案:**如果异常直接冒泡,用户只会看到程序崩溃,模型也没有机会修正参数或解释失败。程序可以把错误类型、是否可重试、建议动作和调用标识写成工具结果,再让模型决定重试、改用查询工具或向用户说明。重试次数、超时和副作用保护仍由代码硬限制,不能交给模型自行决定。
问题:流式返回工具参数时,什么时候才可以执行工具?
**参考答案:**工具名称和
arguments可能跨多个事件分片返回,必须先累积完整字符串,再解析 JSON 并通过 schema 校验。半截 JSON、未知工具、缺少必填字段或超出范围的参数都不能执行。文本 token 可以即时展示,但工具执行期间应显示“正在查询”之类的状态,避免把尚未完成的调用误当成最终答案。
LangGraph
问题:把裸 API 的
for循环拆成 LangGraph 的哪些部分?**参考答案:**消息历史和任务信息进入
state;发起模型请求的是模型节点;执行工具的是工具节点;判断是否存在工具调用的是条件边;工具节点完成后再连回模型节点。这样做没有改变 Agent 的基本语义,只是把原来藏在循环和if/else里的路径显式画出来,便于暂停、恢复和逐节点观察。问题:什么时候使用图反而会让代码更复杂?
**参考答案:**如果只有一个模型、几个只读工具、固定的两三步流程,单个循环更直接。上图后需要定义 state 类型、节点函数、边和运行配置,学习和维护成本都会增加。只有当流程确实包含多分支、回退、人工审批、断点续跑或多个 Agent 共享状态时,显式控制流带来的可读性和可恢复性才值得这份成本。
问题:服务台助手的 state 为什么不应该只是一个越来越大的字典?
**参考答案:**消息、检索证据、待审批动作、错误状态和重试计数的生命周期不同,混在一起容易被节点互相覆盖,也容易把密钥或完整原始响应持久化。应为关键字段定义类型、写入者和读取者,例如
messages保存对话、evidence保存来源摘要、approval保存审批决定,副作用工具只读取已确认的动作。这样才能检查每条边的输入是否完整。问题:创建工单前如何设计人工确认,才能避免重复执行?
**参考答案:**在副作用工具前保存包含任务 ID、参数、风险说明和操作者的 checkpoint,图在这里暂停。批准后恢复到执行节点,并用幂等键或已执行标记防止重复点击造成多个工单;拒绝则走取消路径。进程重启后要能凭稳定的任务 ID 找回状态,审批人、时间、决定和实际结果都应记录。
Agent SDK(官方框架)
问题:Agent、Runner、Session 和 tracing 大致对应裸 API 的哪些代码?
参考答案:
Agent通常封装指令、模型和工具列表;Runner负责启动运行并处理多轮循环;Session保存跨轮或跨请求的上下文;tracing 记录模型、工具、交接和错误事件。它们替代的是重复样板,不会替你设计工具权限、确认规则和业务状态。阅读 SDK 代码时,要标出哪些行为是默认值,哪些仍由自己的回调或中间件控制。问题:为什么要用同一个服务台任务比较 LangGraph 和 Agent SDK?
**参考答案:**换任务会把业务差异和框架差异混在一起。固定相同的工具、输入样本和安全规则后,才能比较状态是否显式、分支是否容易审查、暂停恢复怎么做、运行轨迹能看到什么,以及需要自己维护多少代码。最终输出一样并不代表体验一样,还要比较失败恢复、延迟、工具调用次数和调试信息。
问题:SDK 的 handoff 或子 Agent 为什么不能直接共享全部对话历史?
**参考答案:**全部复制会带来上下文膨胀、隐私泄露和职责混乱。主 Agent 应传递结构化任务包,例如用户问题、已确认的身份、必要证据和期望输出;下游只获得完成职责所需的工具和数据。还要限制交接深度、总预算和可用权限,避免 Agent 之间互相转发形成无穷循环。
问题:怎样给“创建工单”这类工具加一层可靠的 guardrail?
**参考答案:**在真实工具执行前检查用户身份、资源范围、参数枚举、风险级别和审批状态;越权、提示注入或参数篡改直接拦截,缺少信息则让用户补充,已批准的低风险请求才进入执行。guardrail 不是提示词里的提醒,而是代码侧不可绕过的边界,并且要记录拦截原因、审批记录和最终执行结果。
流式回复中断后的恢复
- 问题:为什么重新建立 HTTP 连接,不一定能让模型从 99% 的位置继续生成?
**参考答案:**HTTP 连接属于传输层,连接断开后可以尝试重连;模型生成过程是否支持按事件游标恢复,则取决于具体 API 和网关。很多模型接口没有提供“从第 N 个 token 继续”的能力,重连只能重新发起请求。因此代码需要区分“恢复未收到的事件”和“用原始任务重新请求一段续写”,不能把两者混成一个 retry。
- 问题:已经收到的半段回答应该怎样保存,才能支持恢复又不误当成最终答案?
**参考答案:**按 run_id 保存事件序号、文本片段、最后事件时间、finish_reason、工具调用状态和当前状态。半段文本只能标记为未完成草稿,不能直接写成一条已完成的 assistant 消息,也不能据此执行未完成的工具调用。恢复时把原始输入、已确认的工具结果和草稿前缀交给新的请求,并要求只补缺失部分;追加前还要做重叠检测,避免重复一两句话。
- 问题:如果断点发生在工具调用或 JSON 参数中,为什么不能直接续写那几行字符?
**参考答案:**工具名或 JSON 参数只要缺一部分,就无法确认调用对象和参数边界。程序应丢弃不完整调用,重新请求结构化工具调用,并在执行前重新做 schema、权限和范围校验。如果工具已经成功执行,只恢复最终回答,不要再次执行工具;有副作用的工具还要用幂等键或执行记录挡住重复请求。
- 问题:自动恢复应该设置哪些硬限制?
**参考答案:**至少要限制恢复次数、总耗时、增加的 token 或费用、续写长度和工具重试次数。网络错误可以按指数退避重试,认证错误、权限错误和参数错误不应盲目重试;超过上限后保留已经生成的内容,给用户一个明确的“继续生成”或重新提问入口。每次恢复都要记录原因、起止时间、追加内容长度和是否发生工具调用,方便判断恢复是否真的改善了体验。
流式响应的生产级恢复
- 问题:为什么要把浏览器连接、模型生成和 Response Job 拆成三个生命周期?
**参考答案:**浏览器可能刷新、切网或被代理断开,但后台生成任务不一定应该取消;模型流也可能正常结束,只是浏览器还没收到最后几个事件。把一次回答建模成独立的 Response Job,由 Worker 负责调用模型并写入事件日志,SSE 只负责订阅和重放,才能在浏览器重连时补发已经保存的内容,而不用让模型从头生成。
- 问题:
provider_offset、persisted_seq和client_ack_seq分别解决什么问题?
参考答案:provider_offset 表示 Worker 从模型流读取到哪里,persisted_seq 表示服务端事件日志已经提交到哪里,client_ack_seq 表示浏览器已经处理到哪里。三者的差距能帮助判断内容卡在模型读取、事件持久化还是最后一公里。它们不能都简单叫 seq,否则排查时无法判断某个片段是否已经安全保存或只是暂时发给了客户端。
- 问题:
attempt或 fencing token 为什么能防止旧 Worker 覆盖新结果?
**参考答案:**旧 Worker 可能在网络分区或暂停后恢复,而用户已经启动了新的续写轮次。每次合法启动都递增 current_attempt,Worker 写入事件前检查自己的轮次仍然有效;多实例环境还要把这个检查放到共享存储的条件更新中,必要时使用租约或 fencing token。这样旧 Worker 即使恢复,也不能继续写入新一轮任务。
- 问题:为什么 Redis Pub/Sub 不能单独承担流式事件恢复?
**参考答案:**Pub/Sub 适合把在线事件即时推给订阅者,但离线期间错过的消息不会自动保留和补发。浏览器断线后要按 after_seq 重放,就需要数据库事件表、Redis Streams、Kafka 等可回放存储;生产实现还要明确正文追加、事件写入、序号分配和状态更新的事务边界,避免正文已经提交但事件通知丢失。
- 问题:生产环境为什么要同时测试浏览器断线、进程重启和两个 Worker 竞争?
**参考答案:**浏览器断线只验证 SSE 重连和事件重放;进程重启验证任务状态与事件是否真正持久化;两个 Worker 竞争则验证幂等、租约和 fencing 是否挡住过期写入。三类故障分别覆盖传输层、存储层和并发控制层,只测试其中一种,不能说明整条恢复链路可靠。
完成后的自测
当你能不看答案完成下面三件事,这一章才算真正读完:
- 用裸 API 让助手分别完成知识库查询、工单查询和直接回答,并能解释每条消息的作用。
- 用 LangGraph 画出同一任务的节点和条件边,说明一次工具失败或人工拒绝会走哪条路径。
- 用 Agent SDK 跑通同一组输入,列出它替你接管的代码,以及仍必须由业务代码负责的权限、幂等和错误边界。