LangChain 05:记忆、流式、人工审批与生产治理
1、本篇任务:把“退款建议”变成可恢复、可审批的流程
现在我们有政策检索和订单查询。本课增加三项产品能力:同一会话记住之前的订单;运行过程实时显示;真正提交退款前暂停并等待人工决定。
这三项能力共享同一个基础:运行状态必须在服务端持久化。 浏览器内的聊天数组不是记忆,逐 token 显示不是恢复,确认按钮也不是权限系统。
2、先区分三种状态
| 状态 | 例子 | 保存位置 |
|---|---|---|
| 短期会话状态 | 消息、工具结果、当前中断点 | checkpointer,以 thread_id 组织 |
| 长期用户记忆 | 回复语言、用户明确保存的偏好 | user-scoped store |
| 业务事实 | 订单、退款记录、政策版本 | 业务数据库/知识库 |
不要把订单状态复制进长期记忆。用户下次回来应重新查询权威订单服务,而不是相信几天前的摘要。
3、用 thread 和 checkpointer 保存短期记忆
Python 本地演示:
from langgraph.checkpoint.memory import InMemorySaver
from langchain.agents import create_agent
agent = create_agent(
model="openai:gpt-5.5",
tools=[get_order, search_policy],
checkpointer=InMemorySaver(),
)
config = {"configurable": {"thread_id": "conversation-42"}}
agent.invoke({"messages": [{"role": "user", "content": "查 A100"}]}, config=config)
result = agent.invoke({"messages": [{"role": "user", "content": "它还能退吗?"}]}, config=config)
TypeScript 使用相同原则:编译 Agent 时配置 checkpointer,调用时传稳定 thread ID。InMemorySaver/MemorySaver 只适合测试;生产需要数据库持久化,并设置租户权限、TTL、加密、备份和删除机制。
thread ID 只是游标,不是授权。每次读取或恢复 thread 前,都要验证当前用户拥有它。
4、长会话不是无限追加消息
消息越来越多会抬高成本、降低注意力并超过上下文窗口。处理顺序应是:保留最近关键原文;删除无用的大型工具输出;对旧消息做带范围的摘要;把权威事实重新从工具读取。
摘要要记录覆盖范围和生成版本,例如 summary covers message 1-24。用户纠正事实后,要更新摘要或标记旧事实失效。不要把“删除早期消息”伪装成记忆管理。
5、流式输出要传运行事件
至少区分四类事件:
type AgentEvent =
| { type: "text.delta"; runId: string; sequence: number; text: string }
| { type: "tool.started"; runId: string; sequence: number; tool: string }
| { type: "approval.required"; runId: string; sequence: number; action: RefundDraft }
| { type: "run.completed"; runId: string; sequence: number; answer: string }
| { type: "run.failed"; runId: string; sequence: number; code: string; retryable: boolean };
裸 token 只能做打字机效果。runId + sequence 才能在断线重连后补发并去重;工具和审批事件让前端呈现真实状态。框架 stream chunk 应由后端适配为稳定产品事件,不直接透传内部对象。
6、退款必须先生成草案,再等待审批
get_order(只读)
→ search_policy(只读)
→ draft_refund(只生成金额、理由、action_id)
→ interrupt:展示草案
→ 用户 approve / edit / reject
→ submit_refund(重新鉴权、幂等提交)
LangChain 的 HITL middleware 可以按工具配置中断;LangGraph 的 interrupt() 可以在自定义节点中精确暂停。无论使用哪种接口,审批 payload 必须可序列化、可显示,并绑定 action_id、参数 hash、审批人和过期时间。
批准后后端仍要重新验证订单所有权、可退款状态和金额。浏览器发回的 preview 与模型参数都不是权威数据。
7、幂等解决最危险的失败窗口
网络可能在“退款服务已成功、Agent 尚未写最终状态”时断开。恢复后若再次提交,就会重复退款。解决方法是在提交时使用数据库唯一的 action_id:第一次执行写结果;后续相同 ID 返回第一次结果。
def submit_refund(actor, action_id, order_id):
existing = refund_repo.find_by_action(action_id)
if existing:
return existing
with transaction():
order = order_service.lock_for_actor(actor, order_id)
refund = refund_repo.create_unique(action_id, order)
outbox.publish_unique(action_id, "refund.requested", refund)
return refund
8、Middleware 应只处理横切规则
适合 middleware 的是模型重试、动态模型选择、调用预算、上下文压缩、PII 脱敏、HITL 和日志。订单归属、退款期限和金额计算属于领域服务,不应藏进 middleware 或 system prompt。
推荐顺序:输入限制 → 身份/context 注入 → 上下文裁剪 → 模型/工具预算 → HITL → 输出校验与脱敏。每一层都要有独立测试。
9、本篇验收
刷新页面后会话仍在;相同 thread 的追问能理解“A100”;断线重连文本不重复;未批准时业务库没有退款;同一审批重复提交十次只生成一条退款;其他用户无法读取或恢复该 thread。
下一课把这些能力装进一个清晰的后端项目,而不是继续堆在单文件示例里。
如果您觉得这篇文章有帮助,请点个赞吧~
评论
请登录后发表评论
去登录