跳转到内容

第 47 章 理解 Agent 的起点:工作流、状态与记忆

员工问:现在去北京出差,住宿限额是多少?第 45 章的本地 RAG 流程已经能找到现行制度,带着引用回答每间夜 750 元。现在,我们允许助手在“不满意”时继续搜索。它找到同一份文件,问自己是否还要查,再次搜索。答案本来并不难,处理过程却变得难以控制。

缺少的不是再调用一次模型,而是三件可以说清楚的事:下一步允许做什么,这一步之后留下哪些信息,整个过程为什么会停止。第 42 章已经把“模型提出操作”和“应用执行操作”分开。本章把这条界线扩展到整个请求:先让处理路线可以检查,再把其中一处选择交给模型。

先固定任务与验收标准:依据适用的制度回答现行限额,保留单位和引用,然后结束。金额虽然正确,来源却已经过期,不算通过;搜索一直不停,只在中途打印过一次 750,也不算通过。

单次调用(single call)由应用把问题和制度一并交给模型,调用一次后接收答案。提示词里提到某份文件,不等于模型已经拿到文件;总得有人把内容放进输入。调用链(chain)则事先规定顺序:检索制度、组装输入、生成答案。检索这一步本身未必需要模型。

工作流(workflow)进一步加入由应用规定的分支。来源缺失,就暂不作答;找到了来源,还要检查生效日期。工作流可以有循环,但进入条件和次数上限应当写清楚。Agent 则可以根据当前观察选择下一步:换一处来源搜索、调用工具,或者结束。这里有用的区别是交出了多少选择权,不是有没有循环,也不是模型总共调用了几次。这是本书分析系统时采用的工作定义,不表示所有程序库都使用完全相同的命名。

单次调用
  1. 模型之外问题+给定制度
  2. 处理步骤一次模型调用
  3. 文本片段回答 → 停止

应用在调用前备齐输入。

调用链
  1. 处理步骤检索
  2. 处理步骤组织回答
  3. 应用保存的记录停止

操作顺序事先确定。

工作流
  1. 处理步骤检索 → 核验
  2. 应用保存的记录证据充分?
  3. 文本片段回答或暂不作答 → 停止

应用规定分支和进入条件。

开放决策循环
  1. 应用保存的记录读取当前状态
  2. 处理步骤模型选择工具或结束
  3. 模型之外工具结果 → 再读状态

若宿主不设上限,返回箭头可能一直循环。

KA-5 的实际状态图
  1. 应用保存的记录START → RETRIEVED
  2. 应用保存的记录有效 → VERIFIED → DECIDED
  3. 文本片段use_evidence → COMPOSED → ANSWERED

证据无效 → ABSTAINED;选择 abstain → ABSTAINED;选择非法、超时或预算耗尽 → STOPPED。终止状态没有后续转移。

图 47.1:下一步由谁决定? 原创教学图示 · CC BY-SA 4.0

图 47.1 始终处理同一个住宿问题,只改变谁来决定下一步。开放循环中,工具返回结果之后,箭头又回到读取状态的位置。如果新结果要求调整计划,这条返回路径很有用;如果每次都得到相同的失败结果,它也可能让资源一直消耗下去。即使由模型选择操作,应用仍要控制这个循环。

编排器(orchestrator)就是负责调用步骤、记录结果,并决定下一步是否允许执行的应用代码。本项目的编排器是一个很小的 Python 函数,并不是另一个模型。大部分计划已经写进固定状态图:工具提供观察结果,状态记录进展,终止状态结束请求。ReAct 论文给出了语言处理与环境操作交替进行的研究实例,但采用这种模式,并不会自动得到本书需要的权限检查和停止保证。章末文献说明了两者的联系与界限。

例 47.1:沿现行制度走到终止状态。 输入是“北京住宿”,问题日期为 2026-09-14,主题为差旅。沿用的资料库同时保留两份制度:travel-v1 在 2025 年规定 600 元;travel-v2 自 2026-01-01 起规定 750 元,并替代 v1。两份制度的金额单位都是元/间夜。资料顺序和检索排序仍按第 44—46 章的规则。

应用先检索适用资料,检查支持结论所需的完整原文,再询问模型。给模型的候选按顺序固定为 use_evidence、abstain。模型看到问题、所选资料、两个候选和一段简短的选择说明。输入中没有取消工具,也没有凭据。返回值必须与某一个候选完全相同,不能另选“再搜索一次”。

表 47.1:KA-5 成功请求中,每个状态来自哪一步。

记录的状态刚刚发生了什么下一步凭什么可以执行?
RETRIEVED选中了 v2 的第 1—3 行已有来源,但仅有来源还不够
VERIFIED日期、来源、完整范围与摘录结论均通过检查宿主允许进行一次受限选择
DECIDED模型返回 use_evidence返回值属于给定的两个候选
COMPOSED本地程序准备好 750 与完整引文还要通过最后一次来源支持检查
ANSWERED返回检查通过的答案没有下一步;这是终止状态

这三行原文要合起来看:第 1 行给出单位,第 2 行给出生效日期和替代关系,第 3 行才给出 750。只留下最后一行,就丢掉了这个数字为什么适用的依据。回答仍按第 45 章的原文摘录契约组织,不再调用另一个模型自由发挥。核验时会先准备一个候选答案,便于检查它将要作出的结论;只有模型完成选择、答案又通过最后关口,应用才把它交出去。

这条路径记录了五次状态转移。配置允许最多八次,其中也包括写入记录的停止转移。START 离开初始状态之前,计数为零;完成五次之后,还有三次额度没有使用。流程到了终点,并不需要把额度花完。模型调用另有一个上限:一次。选择失败也算用掉了这一次,不能悄悄再问一遍,把一个决策点变成两个。

现在可以给表中的规则写一个简短记号。用 sts_t 表示第 tt 步之后记录的状态,用 oto_t 表示做下一步之前可以看到的结果。应用按下面的规则改变状态:

st+1=T(st,ot).s_{t+1}=T(s_t,o_t).

这里的 TT 是写好的状态图及其进入条件,不是模型学到的参数矩阵。例如,处于 VERIFIED 时看到 use_evidence,可以进入 DECIDED;看到不认识的值,就进入 STOPPED。下标数的是步骤,不是秒数。沿一条边前进之前必须满足的条件,叫作守卫条件(guard);没有后续转移的状态,叫作终止状态(terminal state)。固定数据列出了全部转移,读者不用运行 Python,也能核对这些约束。

本机 Qwen3-1.7B 的实际记录中,中英文两次选择都返回了 use_evidence,相应的宿主流程也都带着 750 到达 ANSWERED。这两次观察说明模型已经接入流程,不能据此断言它能可靠判断证据。核心测试还会人为给定不同的选择字符串,以稳定地走到各条分支;那是在检查软件行为,不是增加了几次模型测量。

只改一个条件:把资料库换成空集合。应用依次记录 RETRIEVED、ABSTAINED,原因为 insufficient_evidence,模型调用次数为零。没有来源已经是应用可以直接识别的停止理由;再请模型表态,只会增加开销,不会凭空产生证据。

资料有效,但模型选择 abstain,路线就是 RETRIEVED → VERIFIED → DECIDED → ABSTAINED。若返回 cancel_order,路线则是 RETRIEVED → VERIFIED → STOPPED。后者只是非法的选择数据,始终不会变成可执行命令。决策点发生超时,也会结束请求。保留这些不同记录,维护者才知道应该查资料缺失、模型判断,还是返回值不符合约定。

再把转移额度从八次改成两次。检索和核验用完这两次,应用便在调用模型之前停止。即使已经没有空间再追加一条事件,返回结果中的最终状态和原因仍会写明预算耗尽。因此,阅读事件列表时必须同时看最终状态,不能把列表当成全部结果。第 49 章会补上持久事件记录和调用预算,使进程失败后的恢复也有依据。

目前的图没有循环,因此图的结构本身就能避免无穷工具调用。额度上限仍有价值:以后修改路线时,它能继续约束新路径。如果加入重试,每次重试也必须消耗有限预算,或受到截止时间的约束。“模型知道什么时候该停”不是应用能保证的性质。宿主在每一条合法执行路径上都保持成立的条件,才叫作不变量(invariant),例如模型调用次数始终不超过一。

回答之后,我们可以把“750”存起来,明天再用。这样又出现了一个问题:究竟存了什么,由谁在什么时候读取?第 2 章区分过“信息已经保存”和“信息已经进入模型输入”。第 11 章的循环状态则把前面的序列信息压缩成一组数值。两者都不会自动成为可靠、持久的任务记录。

表 47.2:这次请求中,不同数据的存续范围与责任。

数据一次请求之内多次请求之间怎样才能继续使用?
上下文(context)交给模型的问题与 v2 引文通常重新组装;对话记录可以另行保存应用再次选择并传入
记忆(memory)取出的笔记可以加入上下文应用保存的笔记或偏好可以持续存在规定范围、来源、权限、过期和取用规则
RNN 隐藏状态(hidden state)第 11 章中概括序列的数值只有实现明确保留时才会延续需要相应架构与重置规则;它不是订单账本
Agent/工作流状态(state)当前阶段、证据 ID、选择和剩余预算明确持久化后,才能跨越进程失败需要数据结构、检查点与恢复规则
缓存(cache)复用已有检索或计算结果可以保留到下一次请求,直到失效缓存键要覆盖所有会改变结果的输入

缓存与偏好记忆的目的不同。缓存用于避免重复计算;记忆则可能有意影响下一次请求。它们都是保存的数据,也都可能过时。“这位员工以前按 600 元安排住宿”说明过去的一次交互,不能成为今天制度的权威来源。

假设缓存键只有“北京住宿”这几个字。即使新的检索代码没有问题,缓存也可能在 2026 年返回 2025 年的答案。对这个小计算而言,合适的缓存键应包含问题、问题日期、主题、语言、资料库版本和处理程序版本。如果不同租户或用户能访问的资料不同,还要把有关授权范围纳入键中,或者在复用前重新检查权限。第 53 章会实际展示租户信息遗漏后的结果。

保存记忆也可能保存错误或攻击。摘要若把“文件谎称已经获得许可”压缩成“已经获得许可”,文字虽然短了,意思却变了。因此,KA-5 默认不保存跨请求的偏好记忆,也不声称自己能可靠地长期记事。加入记忆是一项数据管理选择,需要先说明用途和删除规则,不能把它当成免费的可靠性升级。

读过 Primer-PY 和 Primer-HTTP 后,可以查看共享实现 code/knowledge-assistant/workflow.py。状态图位于 data/knowledge-assistant/ka5-workflow-v1.json,双语选择说明集中放在 ka5-content-v1.json。本部分 Notebook 也使用这些文件。下面给选择位置接入一个返回值固定的测试替身:

result = run("Beijing lodging", choose=lambda _: "use_evidence")
print(result["state"], result["answer"]["amount_yuan"])
ANSWERED 750

这里的 choose 函数只在软件测试中替代模型。另行保存的 model-run.json 才是实际模型请求与输出,还记录了运行程序版本、模型摘要、种子和 CPU 使用情况。读懂前面的完整流程,不需要执行这段程序,也不需要安装模型。

KA-5 的范围很窄:在使用已核验资料和暂不作答之间选择,不能规划任意差旅行程、浏览新来源或取消订单。这样做减少了灵活性,却让停止条件和失败位置可以检查。如果固定工作流已经能完成任务,开放更多 Agent 决策就需要与这条基线比较,说明多出的选择权究竟带来了什么。

单次调用、调用链、工作流与开放 Agent 循环,安排控制权的方式不同。即使把一处选择交给模型,编排器仍负责规定状态转移和额度。本章的成功路径经过五次转移、一次受限模型调用,得到 750;其他路径则带着可以定位问题的原因停止。上下文、记忆、循环状态、工作流状态与缓存的用途和存续时间各不相同,保存一段文字不会自动授予执行权限。

现在,资料来源核验已经是一项可以明确重复的工作。下一章要解决的是:怎样把这套做法封装起来,既不用复制一大段提示词,又不在复用时悄悄改变规则。

  1. 保留有效的 v2 资料,让选择函数返回 use_evidence; search_again。写出状态路线、模型调用次数,并解释是否会发生搜索。
  2. 开发者把“资料缺失”分支改成“再检索一次”,却没有改变任何计数器。构造一条不会停止的执行路线,并给出两种相互独立的修复办法。
  3. 缓存键包含问题、语言和资料库版本,却没有问题日期。使用完全相同的 v1/v2 资料库,构造两个不应共用答案的请求。
  4. 助手在对话摘要中保存了“mira 已批准取消”。这属于哪一种保存的数据?执行写操作之前,还缺少哪些证据?
  1. 路线为 RETRIEVED → VERIFIED → STOPPED,已经调用模型一次。应用拿整个字符串与两个候选比较,只匹配前缀不够。不会调用搜索工具。
  2. 空资料库每次都返回无证据,流程沿重试边回到相同状态,计数器又始终不变,于是可以无限重复。一种修复是删掉这条返回边,直接暂不作答;另一种是每次尝试都扣减有限的重试预算,到零就停。截止时间还能从另一个方向限制总时长。仅在提示词里要求停止,不能落实这些规则。
  3. 分别问 2025-09-14 和 2026-09-14 的住宿限额。资料库字节相同,适用制度却分别给出 600 和 750。资料库版本不能代替用来判断制度适用性的问题日期。
  4. 这是保存的对话记忆,以后可能被放进上下文,但它不是审批记录。写操作需要可信的访问主体、允许的权限范围、具体操作与目标、适用的对象版本、有效审批,以及执行时仍然成立的授权。第 49 章会实现这些检查,不从摘要文字里读取执行许可。