**今日主题:**学习 Prompt 中的 Thought、Action 与 Observation,并通过示例轨迹分析:每一轮增加了什么信息、模型为什么选择当前动作、观察结果怎样改变下一步决策。
学习了 ReAct 的基本思想:模型不是一次性生成答案,而是在推理、行动和环境反馈之间不断循环。今天继续向下拆解,重点不再是记住三个英文单词,而是理解一条 ReAct 轨迹内部的信息是怎样流动的。
**理解:**看懂 ReAct Prompt,不能只看"模型调用了什么工具",还要像看侦探办案一样追踪证据:原来掌握什么、刚刚发现什么、哪条证据改变了判断、为什么此时可以结案。
一、今日学习目标:
- 解释 Thought、Action、Observation 在 Prompt 中分别承担什么职责。
- 区分"模型生成的内容"和"应用或环境返回的内容"。
- 从一条执行轨迹中找出每一轮的信息增量。
- 说明模型选择某个工具和参数的决策依据。
- 判断一次工具调用后应该继续、修正、换工具还是结束。
- 识别重复动作、伪造观察、忽略错误和过早结束等失败模式。
- 使用简洁日志记录决策摘要,而不是依赖展示完整内部推理。
二、ReAct Prompt 不只是一句指令
一个可工作的 ReAct Prompt 通常不只是写一句"请按照 Thought、Action、Observation 思考"。它往往包含以下几类信息:
1. 任务目标
告诉模型最终需要解决什么问题。例如:
任务:根据设备手册判断机械臂是否适合抓取 2 kg 工件,
并给出可核查的计算依据。
目标决定了什么叫"完成"。如果目标只写"查询负载",模型可能查到一个数字就结束;如果目标要求"判断是否适合并给出计算依据",模型还需要完成计算和结论整理。
**理解:**目标就像终点地址。地址只写到城市,导航到城市边界就认为完成;地址精确到楼层和房间,导航才知道还要继续走。
2. 工具说明
告诉模型可以使用哪些 Action,以及每个工具需要哪些参数:
search_manual(query: string)
用途:从设备手册中检索相关参数。
calculator(expression: string)
用途:执行确定性的数学计算。
工具描述越清楚,模型越容易正确选择;如果两个工具的说明高度重叠,模型可能随机选择或反复切换。
3. 行为约束
规定模型什么时候应该调用工具、什么时候可以回答,以及禁止哪些行为。例如:
- 实时数据和设备参数必须通过工具查询,不能凭记忆猜测。
- Observation 中没有出现的数字不能当成已验证事实。
- 工具失败后先阅读错误原因,再决定是否重试。
- 信息足够时立即输出 Final Answer,不要为了凑轮数继续调用。
- 涉及机械臂运动时,语言模型只能提出意图,安全校验和真实执行由应用负责。
4. 示例轨迹
原始 ReAct Prompt 经常通过少量完整示例,让模型模仿 Thought、Action、Observation 和最终答案的顺序。示例真正传达的不只是格式,还包括行为:
- 遇到信息缺口时先查证。
- 收到新信息后更新计划。
- 动作失败时根据错误修正。
- 证据足够时停止循环。
5. 输出和终止格式
如果系统依赖文本解析,需要明确动作和最终答案的格式:
Thought: ...
Action: search_manual
Action Input: {"query": "..."}
Observation: ...
Final Answer: ...
现代系统更常使用结构化 Function Calling 表达 Action,但无论采用文本还是 JSON,都必须让应用能够区分"继续调用工具"和"已经生成最终答案"。
三、Thought:根据已有信息决定下一步
Thought 是模型在当前上下文中形成的决策过程。在原始 ReAct 示例里,它常以文本推理轨迹出现;在工程系统中,更适合保存简洁的"决策摘要",说明信息缺口和下一步目的。
1. Thought 的输入是什么
一轮 Thought 不是凭空产生的,它通常基于:
- 用户原始任务。
- 系统约束和工具说明。
- 之前已经执行的 Action。
- 环境返回的 Observation。
- 剩余步骤、时间和费用预算。
- 权限、安全和业务规则。
2. Thought 应回答哪些问题
一个有效 Thought 至少应该完成以下判断:
- 当前已经掌握哪些可靠信息?
- 距离用户目标还缺哪一项信息或操作?
- 这个缺口能否通过现有工具解决?
- 调用哪个工具最直接?
- 工具参数应该从哪里获得?
- 如果信息已经充分,是否应该停止调用?
3. 好 Thought 与坏 Thought 的区别
| 类型 | 示例 | 问题分析 |
|---|---|---|
| 有效 | 手册负载未知,需要先检索额定负载。 | 明确指出信息缺口和下一步目的。 |
| 有效 | 已知额定负载和末端重量,还需计算安全余量。 | 使用了上一轮新信息,并推动任务前进。 |
| 无效 | 机械臂是一种复杂的自动化设备,需要综合考虑很多因素。 | 内容看似正确,但没有形成可执行决策。 |
| 无效 | 我应该再搜索一次。 | 没有说明缺少什么,也没有说明搜索词为什么要变化。 |
4. Thought 不是越长越好
一段很长的分析不一定比一句简短决策更有价值。判断 Thought 质量的标准应该是:
- 是否引用了当前可用信息。
- 是否指出了具体信息缺口。
- 是否能自然推导出下一步 Action。
- 是否遵守工具、权限和安全约束。
- 是否避免重复已经失败的方案。
**理解:**Thought 像导航中的"决策点",重点是为什么左转、右转或到达终点,而不是把地图上所有道路都描述一遍。能指导下一步的短判断,比没有行动方向的长篇分析更有用。
5. 工程中为什么使用"决策摘要"
原始论文中的 Thought 有助于研究者观察行为,但真实系统不应把展示完整内部推理当作运行前提。建议记录下面这种简洁信息:
{
"known": ["额定负载为 3.0 kg", "末端工具重 0.6 kg"],
"missing": ["保留 20% 安全余量后的可用负载"],
"decision": "调用 calculator 计算",
"reason_summary": "需要确定性计算,避免口算错误"
}
这样的记录足以进行调试和审计,也更容易与业务日志、工具调用和错误信息对应。
四、Action:把决策转换成可执行请求
Action 是模型提出的外部操作。原始 ReAct 常使用文本形式:
Action: Search[mechanical arm rated payload]
现代 Function Calling 则更常使用结构化对象:
{
"name": "search_manual",
"arguments": {
"query": "机械臂额定负载 末端工具重量"
}
}
1. Action 的信息从哪里来
一个合法 Action 的工具名必须来自工具列表;参数通常来自:
- 用户明确提供的信息。
- 之前 Observation 返回的数据。
- 系统或应用注入的可信上下文。
- 模型对自然语言的结构化提取。
模型不能把自己猜测的数值包装成参数后,就把它当作真实环境数据。
2. 选择 Action 的决策依据
模型选择工具时,应该考虑:
- **相关性:**工具能否直接补齐当前信息缺口。
- **可靠性:**确定性计算应优先交给计算器,而不是让模型口算。
- **成本:**能用一次精确查询解决,就不要进行多次宽泛搜索。
- **风险:**查询工具通常是只读的,机械臂运动、删除和付款则有副作用。
- **权限:**即使模型想调用,应用仍需检查是否被授权。
- **可恢复性:**失败后是否能够修改参数或切换工具。
3. 模型提出 Action,不代表已经执行
真正的工具执行必须由应用完成:
模型生成工具名和参数
↓
应用检查工具白名单
↓
应用校验参数、权限和业务规则
↓
应用执行真实函数或接口
↓
应用生成 Observation
**理解:**Action 像员工提交采购申请。提交申请不等于已经购买,系统还要检查预算、权限、供应商和审批状态。模型可以提出"做什么",但不能绕过应用直接宣称"已经做完"。
4. Action 怎样判断是否有效
可以用四个问题检查:
- 它是否针对当前缺失的信息?
- 工具名和参数是否符合约定?
- 参数是否能追溯到用户输入或 Observation?
- 执行这个动作是否让任务更接近完成?
如果连续两次 Action 完全相同,Observation 也没有变化,系统应怀疑 Agent 已经进入重复循环。
五、Observation:环境返回的新事实
Observation 是 Action 执行后由工具、应用或外部环境返回的结果。它不是模型根据常识自行编写的一段话,而是模型与真实环境之间的反馈通道。
1. Observation 可以包含什么
- 搜索命中的文本或证据来源。
- 数据库查询结果。
- 计算器返回的数值。
- 图像识别得到的类别和坐标。
- 机械臂当前状态或动作结果。
- 参数校验错误、超时、权限不足等异常。
- 是否允许重试以及修改建议。
2. 成功 Observation
{
"ok": true,
"data": {
"rated_payload_kg": 3.0,
"end_effector_kg": 0.6
},
"source": "设备手册第 4.2 节"
}
这一结果增加了两个可验证事实,并提供了来源。下一轮 Thought 应该使用这两个数,而不是重新猜测负载。
3. 失败 Observation
{
"ok": false,
"error": {
"code": "INVALID_QUERY",
"message": "query 不能为空",
"retryable": true,
"hint": "请提供设备型号和待查询参数"
}
}
错误同样是新信息。它告诉模型:工具没有执行成功、失败原因是什么,以及是否值得修正后重试。
4. Observation 为什么要可操作
| 返回内容 | 模型获得的信息 | 可能后果 |
|---|---|---|
| 失败 | 几乎没有 | 容易盲目重试。 |
| 参数错误 | 知道大类原因 | 仍需猜哪个字段错误。 |
| query 为空,可补充型号后重试 | 错误位置、修复方法、重试条件都明确 | 模型可以有依据地修正 Action。 |
| 权限不足,不可重试 | 明确知道当前路径无法继续 | 停止调用并请求授权。 |
**理解:**Observation 像考试批改。如果老师只写"错",学生可能继续用原方法;如果写"公式正确,但单位换算错误",学生才知道下一步应该改哪里。
5. 不要把未经执行的内容叫作 Observation
下面这种轨迹是错误的:
Thought: 我需要查询天气。
Action: weather(city="北京")
Observation: 北京明天下雨概率为 80%。
(实际上应用没有调用任何天气服务)
这不是工具增强,而是模型伪造工具结果。正确做法是:模型生成 Action 后暂停,由应用执行工具,再把真实结果回传。
六、三者之间的信息流
理解 ReAct 的关键,是看每一个阶段"读了什么、产出了什么"。
| 阶段 | 主要输入 | 主要输出 | 核心问题 |
|---|---|---|---|
| Thought | 用户目标、历史动作、历史观察、约束 | 下一步决策 | 现在还缺什么? |
| Action | Thought 的决策、工具定义、已有参数 | 工具名和参数 | 用什么动作补齐缺口? |
| Observation | 应用真实执行 Action 的结果 | 成功数据或错误信息 | 环境发生了什么? |
| Next Thought | 新 Observation 加上此前上下文 | 继续、修正、换路或结束 | 新信息改变了什么? |
用户目标 + 工具约束
↓
Thought₁:识别第一个信息缺口
↓
Action₁:调用最合适的工具
↓
Observation₁:增加新事实或返回错误
↓
Thought₂:比较"目标"与"当前信息"
↓
继续 Action₂ / 修改 Action / Final Answer
信息增量是判断轨迹质量的核心
每轮结束后都可以问:
- 这一轮新增了哪个事实?
- 排除了哪个错误方案?
- 确认了哪个参数?
- 任务离完成更近了吗?
如果四个问题都回答不了,这一轮很可能只是消耗 Token 和时间。
七、完整示例:判断机械臂能否安全抓取 2 kg 工件
**用户任务:**根据设备手册判断机械臂是否适合抓取 2 kg 工件。已知需要保留 20% 安全余量,请给出计算过程。
可用工具:
search_manual(query):搜索设备手册。calculator(expression):执行数学计算。
第 1 轮:查询缺失的设备参数
**当前信息:**工件为 2 kg,要求保留 20% 安全余量。
**信息缺口:**不知道机械臂额定负载,也不知道末端工具重量是否需要扣除。
**Thought 决策摘要:**最终判断依赖设备参数,不能凭常识猜测,应先查询手册。
Action:
{
"name": "search_manual",
"arguments": {
"query": "机械臂额定负载 末端执行器重量 负载计算"
}
}
Observation:
{
"ok": true,
"data": {
"rated_payload_kg": 3.0,
"end_effector_kg": 0.6
},
"source": "设备手册第 4.2 节"
}
本轮信息变化:
- 新增"额定负载为 3.0 kg"。
- 新增"末端执行器重 0.6 kg"。
- 判断问题从"缺少参数"转变为"需要计算可用安全负载"。
**为什么不能结束:**虽然查到参数,但还没有扣除末端重量和安全余量,因此尚未完成用户要求。
第 2 轮:计算安全可用负载
**当前信息:**额定负载 3.0 kg,末端执行器 0.6 kg,安全余量 20%。
**信息缺口:**扣除末端工具并保留余量后,到底还能承载多少。
**Thought 决策摘要:**这是确定性计算,使用计算器比模型口算更可靠。
Action:
{
"name": "calculator",
"arguments": {
"expression": "(3.0 - 0.6) * (1 - 0.20)"
}
}
Observation:
{
"ok": true,
"data": {
"result": 1.92,
"unit": "kg"
}
}
本轮信息变化:
- 新增"规划使用的安全可用负载为 1.92 kg"。
- 2 kg 工件大于 1.92 kg,已经具备判断依据。
- 所有数据都能追溯到用户输入、手册 Observation 或计算器结果。
第 3 轮:满足退出条件并生成答案
**Thought 决策摘要:**设备参数、计算过程和比较结果都已齐全,不需要继续调用工具。
Final Answer:
按当前约束,不建议抓取 2 kg 工件。
计算过程:
1. 额定负载:3.0 kg。
2. 扣除末端执行器:3.0 - 0.6 = 2.4 kg。
3. 保留 20% 安全余量:2.4 × 0.8 = 1.92 kg。
4. 工件重量 2.0 kg,大于安全可用负载 1.92 kg。
因此,在该安全策略下应更换更轻的末端工具、
降低工件重量,或选用额定负载更高的机械臂。
真实执行前仍需按照设备厂商手册和现场安全规范复核。
示例中的决策依据汇总
| 轮次 | 当时缺少什么 | 为什么选这个 Action | Observation 改变了什么 |
|---|---|---|---|
| 第 1 轮 | 额定负载与末端重量 | 这些是手册事实,必须查询 | 获得两个设备参数 |
| 第 2 轮 | 安全可用负载 | 需要确定性数学计算 | 得到 1.92 kg |
| 第 3 轮 | 没有关键缺口 | 信息充分,不再调用工具 | 生成可核查结论并结束 |
**理解:**这三轮就像做一道应用题:第一轮找题目缺失的数据,第二轮套用明确公式,第三轮比较结果并写结论。每一步都增加了答案所需的信息,没有为了显示"智能"而多走一步。
八、Observation 怎样进入下一轮 Prompt
工具执行完成后,应用需要把 Observation 连同之前的上下文再次发送给模型。概念上,下一轮输入类似:
System:
你是一个使用工具解决任务的助手。
设备参数必须查询,计算必须使用 calculator。
User:
判断机械臂能否安全抓取 2 kg 工件,保留 20% 余量。
Assistant Action:
search_manual({"query": "机械臂额定负载 末端执行器重量"})
Tool Observation:
{"rated_payload_kg": 3.0, "end_effector_kg": 0.6}
Assistant:
根据最新 Observation 决定下一步。
1. 必须保留动作与结果的对应关系
如果一次响应中有多个工具调用,应用必须使用调用标识把每个结果对应回正确 Action。否则模型可能把计算器结果误认为搜索结果,或者漏掉某个工具返回值。
2. 不要只传最后一个结果
下一轮既需要最新 Observation,也可能需要用户目标和此前关键事实。如果只把最后一个数字发给模型,它可能不知道这个数字的来源、单位和用途。
3. 控制上下文长度
轨迹越来越长时,不能无限重复所有原始网页和日志。可以保留:
- 用户原始目标。
- 仍然有效的约束。
- 工具调用和关键结果。
- 错误原因与已尝试方案。
- 经过验证的阶段性摘要。
大段无关文本、重复结果和完整调试堆栈应该截断或放到外部日志中。
4. Observation 不应夹带指令污染
搜索网页或外部文档返回的内容可能包含"忽略之前指令"等恶意文本。应用应把它当作不可信数据,而不是系统命令;工具结果不能自动提升为比系统约束更高的权限。
**我的理解:**把 Observation 放进下一轮 Prompt,像把检查报告放回病历。既要保留报告内容,也要保留患者是谁、要解决什么问题;同时不能因为报告里写了一句"立刻出院",系统就跳过医生和安全规则。
九、如何分析一条示例轨迹
拿到任何 ReAct 日志,都可以按照下面六步分析。
1. 标记最终目标
先把用户真正要的结果写成一句可验证的话。目标不清楚时,后面的"是否完成"就无从判断。
2. 列出初始已知信息
区分用户明确提供的事实、系统提供的上下文和模型自己的猜测。只有前两类可以直接作为可信输入。
3. 找出每轮信息缺口
检查 Thought 是否明确说明"还缺什么"。如果没有信息缺口却仍然调用工具,可能是过度调用。
4. 检查 Action 与缺口是否对应
例如缺少设备额定负载,却调用天气工具,说明工具选择错误;查询词过于宽泛,也会降低 Observation 质量。
5. 标记 Observation 的信息增量
用一句话写出它新增、确认或否定了什么。错误 Observation 则要标记它排除了哪条路径。
6. 检查下一轮是否真的更新
如果下一轮仍然使用相同参数重复调用,说明模型可能忽略了 Observation;如果根据错误提示修改了字段,则说明反馈闭环有效。
十、循环退出条件
ReAct 不能只定义"怎样继续",还必须定义"怎样结束"。
1. 正常完成
用户目标所需的事实、计算或操作结果都已得到,模型输出 Final Answer。
2. 需要用户补充信息
例如用户说"把那个杯子放过去",但场景中有多个杯子和多个放置区域。正确做法是请求澄清,而不是随机选择。
3. 不可恢复错误
例如权限不足、设备急停、机械臂故障或工具不存在。系统应停止执行并说明原因。
4. 达到循环或时间上限
模型一直没有完成时,应用必须使用最大步数、最大工具调用次数或超时终止,并返回已经完成的阶段性结果。
5. 检测到重复动作
相同工具和相同参数连续出现,且 Observation 没有变化,应触发重复动作保护。
6. 需要人工确认
删除、付款、发送消息或真实机械臂运动等有副作用的操作,应在执行前进入人工确认或安全控制流程。
**理解:**退出条件像考试收卷规则。答完可以主动交卷,题目信息不足可以举手询问,设备故障可以报告异常,时间到了必须停止。不能因为学生一直写,就允许考试永远不结束。
十一、典型失败案例
失败案例 1:重复搜索,没有信息增量
Thought: 需要查额定负载。
Action: Search[机械臂负载]
Observation: 未找到具体型号信息。
Thought: 再搜索一次。
Action: Search[机械臂负载]
Observation: 未找到具体型号信息。
**问题:**第二轮没有根据"缺少型号"这一反馈修改参数,也没有询问用户,属于无效重复。
**正确修正:**请求设备型号,或者把型号加入搜索词。
失败案例 2:忽略错误类型,盲目重试
Observation:
{"code": "PERMISSION_DENIED", "retryable": false}
Thought: 工具失败,我再试一次相同调用。
**问题:**权限错误不是临时网络错误,重复调用不会解决问题,还可能产生告警。
**正确修正:**停止执行,提示用户授权或选择不需要该权限的替代路径。
失败案例 3:伪造 Observation
模型生成 Action 后,应用尚未执行工具,模型却自行补写了结果。这会让系统把幻觉误认为真实数据。
**正确修正:**模型输出 Action 后必须暂停,由应用执行,再把真实工具消息回传。
失败案例 4:拿到部分信息就过早结束
查到额定负载为 3 kg 后直接判断可以抓 2 kg,却忽略末端执行器重量和安全余量。
**正确修正:**退出前使用完成条件清单,逐项确认用户要求是否全部满足。
失败案例 5:Thought 与 Action 不一致
Thought 说要查询设备手册,Action 却调用普通网页搜索;或者 Thought 说只读检查,Action 却执行真实运动。
**正确修正:**应用不能只信文字说明,应根据工具白名单、权限和副作用级别校验真实 Action。
十二、一个可复用的 ReAct Prompt 模板
你是一个通过工具解决任务的助手。
【目标】
准确完成用户任务;信息不足时不得猜测。
【工具规则】
1. 只调用工具列表中的工具。
2. 工具参数必须来自用户输入、可信上下文或 Observation。
3. 模型只提出 Action,应用负责真实执行。
4. 工具失败后先分析错误,再决定修正、换工具或停止。
5. 信息充分时立即输出 Final Answer。
6. 达到最大步骤或遇到不可恢复错误时停止。
【每轮决策摘要】
- 当前已知:
- 当前缺口:
- 下一步目的:
- 选择该动作的依据:
【Action】
按工具协议输出工具名和参数。
【Observation】
由应用写入成功数据或错误,不得由模型伪造。
【结束】
满足任务目标后输出简洁、可核查的 Final Answer。
十三、最小执行循环伪代码
def run_agent(task, max_steps=5):
history = [task]
for step in range(max_steps):
decision = model.decide(history, tools)
if decision.type == "final":
return {
"status": "completed",
"answer": decision.answer
}
if decision.type != "tool_call":
history.append({
"observation": {
"ok": False,
"code": "INVALID_MODEL_OUTPUT"
}
})
continue
result = execute_tool_safely(
decision.tool_name,
decision.arguments
)
history.append({
"action": decision,
"observation": result
})
if result.get("retryable") is False:
return {
"status": "failed",
"reason": result.get("code")
}
return {
"status": "stopped",
"reason": "max_steps_exceeded",
"partial_result": summarize(history)
}
阅读这段代码时,要注意模型只负责 decide,真实工具调用由 execute_tool_safely 完成。每次结果都追加到 history,下一轮模型才能看到 Observation。
十四、推荐的轨迹日志结构
{
"run_id": "run_001",
"step": 2,
"goal": "判断是否能安全抓取 2 kg 工件",
"known": {
"rated_payload_kg": 3.0,
"end_effector_kg": 0.6,
"safety_margin": 0.20
},
"missing": ["safe_payload_kg"],
"decision_summary": "调用计算器得到安全可用负载",
"action": {
"name": "calculator",
"arguments": {
"expression": "(3.0 - 0.6) * 0.8"
}
},
"observation": {
"ok": true,
"data": {
"result": 1.92,
"unit": "kg"
}
}
}
这种日志能支持:
- 回放每一步的输入和结果。
- 统计工具成功率和调用耗时。
- 发现重复调用和无效步骤。
- 验证模型是否使用了上一轮信息。
- 区分模型决策错误、参数错误和工具执行错误。
十五、今日练习
练习 1:给轨迹做信息标注
从 ReAct 官方仓库的 hotpotqa.ipynb 或 FEVER.ipynb 中找一条轨迹,为每轮补充:
- 当前已知。
- 当前缺口。
- Action 的选择依据。
- Observation 的信息增量。
练习 2:修复重复调用
设计一个搜索无结果的场景,分别写出:
- 错误做法:使用完全相同的参数重复调用。
- 正确做法:修改查询、换工具或询问用户。
练习 3:把文本 Action 改成 Function Calling
原始文本:
Action: Search[ReAct observation feedback]
改写为:
{
"name": "search_docs",
"arguments": {
"query": "ReAct observation feedback"
}
}
练习 4:设计退出条件
为自己的最小 Agent 写出至少四种终止状态:
completed:任务正常完成。needs_input:需要用户补充信息。failed:遇到不可恢复错误。max_steps_exceeded:达到循环上限。
验收标准
- 能指出每轮新增了什么信息。
- 能解释工具选择与信息缺口的对应关系。
- 能识别 Observation 是真实返回还是模型编造。
- 能判断下一步应继续、修正、换路或结束。
- 能写出一条不超过三轮且每轮都有信息增量的轨迹。
十六、今日小结
- **Thought:**读取目标、历史和约束,判断当前信息缺口与下一步。
- **Action:**把决策表达成明确的工具名和参数,但不代表已经执行。
- **Observation:**由应用或环境返回真实结果,包括成功数据和错误。
- **信息变化:**每轮都应该新增事实、排除错误路径或完成必要操作。
- **决策依据:**来自目标、信息缺口、工具能力、风险、预算和反馈。
- **退出条件:**正常完成、需要补充、不可恢复错误、循环超限或人工确认。
**理解:**ReAct Prompt 的价值,不是要求模型反复输出 Thought、Action、Observation 三个标签,而是建立一条可以核查的反馈链:模型根据现有证据做决定,应用执行动作,环境返回事实,下一次决定必须尊重这个事实。只要后一轮真正因前一轮 Observation 而改变,ReAct 循环才不是形式上的表演。