外观
第 3 章 结构化输出与推理骨架
模型的输出多数不是直接给人看的:它要写进数据库,要作为 if-else 的判断依据,要当参数传给下一个函数,字符串的格式稍有偏差,后面的处理就会报错;模型在回路里每一步的判断是怎么来的,从外部同样看不到,出了错只能猜。本章处理这两件事:3.1 讲结构化输出(Structured Outputs),按照 Schema 约束模型回复的格式,让回复变成程序能可靠解析、拿到就能直接使用的数据;3.2 讲思维链(Chain-of-Thought),让模型把推理过程写出来;3.3 讲三种进一步利用推理过程的做法,反思(Reflection),让模型检查自己上一步的输出并修正,自洽性让模型多答几遍,多数答案说了算,思维树在推理的每一步都做一次评估再往下走,反思部分会介绍代表框架 Reflexion 及它真正有效的条件,后两种做法调用次数多、成本高,只有值得花几十次调用的问题才用得上;3.4 把推理和行动接成显式的循环,即推理加行动(ReAct),Thought、Action 和 Observation 交替进行,这套循环模式是智能体机制的基本骨架。程序能不能可靠地使用模型的输出,推理过程能不能看到,是函数调用、协议和多智能体编排这些后续机制的共同前提。
3.1 结构化输出
让模型抽取数据,第一个要回答的问题是结果以什么形式返回。以舆情平台为例,它每天要处理上万条用户评论,每条抽取平台、情感倾向和涉及的产品三个字段,抽取结果进入数据仓库,报表和告警都依赖它;这类工作以前靠规则加人工完成,现在交给了模型。
最直接的做法,是在提示词里写一句“请以 JSON 格式输出”。这么跑下来,十次里有八九次正常,但某次批处理里可能出现这样的回复:
text
好的,这条评论的分析结果如下:
{"platform": "微博", "sentiment": "负面", "product": "空气炸锅"}
如需调整字段口径,请告知。json.loads 在第一个字符就抛出异常,整条流水线随之停止。模型并没有出错,这条回复只是在 JSON 前后各多了一句话,但对解析器来说,整段内容都是非法输入。第 1 章讲过,模型逐个词元采样生成,输出是概率性的。提示词里的格式要求只能提高“输出合规 JSON”的概率,无法把概率提到 100%。而工程上需要的是每一次都合规,不是大概率合规。
没有约束时,模型做的事只有一件:续写出概率上合理的后续文本。从这个角度看,“输出 JSON”和“用中文回答”没有本质区别,都只是影响续写方向的软性指令。
靠概率起作用的软性指令,无法避免两类失败。第一类是格式错误:markdown 代码围栏、单引号、末尾多余的逗号和夹在中间的注释,任何一个都会让 json.loads 抛出异常。第二类是内容错误,更难处理:JSON 本身合法,字段也齐全,但取值不在约定的范围内。例如 platform 返回了“新浪微博”,解析不会报错,这条脏数据就进了数据仓库,直到报表数字对不上才暴露出来。
解决办法是把软性指令换成强制约束。业界把这类能力统称为结构化输出。在工程实现上可以分成三个层次,约束一层比一层强。
第一层仍然靠提示词:把字段说明和格式示例写全,最好再附上一两个输出样例。这样能明显降低出错率,但本质上仍是概率性的,出错的可能一直存在。它可以作为基本做法,不能作为唯一的防线。
第二层是接口自带的 JSON mode。请求里把 response_format 参数设为 json_object 类型,服务端就保证返回的是一段合法 JSON,json.loads 这一步基本都能直接通过。DeepSeek 的接口文档写明了使用条件:提示词里要出现 json 这个词,要给出期望格式的示例,max_tokens 要设得足够大。文档还提醒,这个模式偶尔会返回空内容,遇到了就调整提示词再试一次。
需要注意,JSON mode 只保证“合法”,不保证“符合业务结构”,字段名和取值仍然可能不符合要求。第 2 章 2.3 节中的 arguments 连合法 JSON 都不能保证;到了这一层,合法性不再是问题,问题变成了“这是不是我想要的那种 JSON”。
第三层是按 JSON Schema 约束输出。JSON Schema 是用 JSON 描述数据结构的规范。在这里它不再只是文档,而是解码时真正生效的约束:服务端在采样阶段就会排除不符合 Schema 的词元。
OpenAI 在 2024 年 8 月把这套机制正式加入 API,名称就叫 Structured Outputs。它有几条硬性要求:根节点必须是 object,additionalProperties 必须设为 false,也就是不允许出现约定之外的字段。按这些要求编写 Schema,服务端就保证输出符合 Schema。
中文厂商也支持了这一层。Kimi 的接口支持 json_schema 类型的 response_format,strict 默认开启,字段约束的细则写在它的 MFJS 规范里。MFJS 全称是 Moonshot Flavored JSON Schema,是 JSON Schema 的一个子集。这份规范和开源校验器 walle 一起放在 GitHub 上,可以查阅,也可以用来校验。
先把前两层写成一段能运行的代码,Schema 层约束的写法在后面单独看。场景继续用舆情抽取,主线使用 DeepSeek 的 JSON mode,重点是解析失败之后怎么处理,也就是重试与修复。
python
import json
import os
from openai import OpenAI
client = OpenAI(
api_key=os.environ["DEEPSEEK_API_KEY"],
base_url="<DeepSeek 兼容端点,见官方文档>",
)
MODEL = "deepseek-v4-flash"
PLATFORMS = ("微博", "贴吧", "知乎", "小红书", "B站")
SYSTEM_PROMPT = """你是舆情平台的评论抽取组件,只输出 JSON,不输出任何其他文字。
三个字段都必须出现,格式示例:
{"platform": "微博|贴吧|知乎|小红书|B站", "sentiment": "正面|中性|负面", "product": "产品名或 null"}"""
def extract(comment: str, max_retries: int = 2) -> dict:
messages = [
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": f"请分析以下评论:\n{comment}"},
]
for _ in range(max_retries + 1):
response = client.chat.completions.create(
model=MODEL,
messages=messages,
response_format={"type": "json_object"},
temperature=0,
max_tokens=200,
)
raw = response.choices[0].message.content
try:
data = json.loads(raw)
except json.JSONDecodeError as e:
# 报错原文回填,让模型修上一轮的输出
messages += [
{"role": "assistant", "content": raw},
{"role": "user", "content": f"上面的输出不是合法 JSON:{e.msg}。只输出修正后的 JSON。"},
]
continue
if data.get("platform") not in PLATFORMS:
messages += [
{"role": "assistant", "content": raw},
{"role": "user", "content": f"platform 只能取 {PLATFORMS} 之一。只输出修正后的 JSON。"},
]
continue
return data
raise ValueError(f"重试用尽,仍未得到合格输出:{raw}")
if __name__ == "__main__":
print(extract("空气炸锅到手就坏,客服还踢皮球,微博上曝光!"))这段代码中最有价值的部分是修复回路。解析失败后,不应该只是报警了事。在生产环境里,让模型重新生成需要额外付费,在此之前可以先做一道几乎不花钱的处理:机械修复。多余的逗号、没有闭合的括号这类小问题,程序自己就能修好,修好后直接放行,可以省下一次模型调用。常见做法有:给截断的 JSON 补上缺少的括号和引号,去掉混进来的 markdown 围栏和前后多余的文字。这些都只需要几行确定性的代码。社区里也有现成的 JSON 修复库,可以直接使用,不必从正则表达式写起。
机械修复处理不了的,才走代码里的这条路径:把报错信息连同上一轮的输出一起追加到消息列表,再请求一次。模型能看到自己错在哪里,修正格式对它来说不难,第二轮基本都能通过。JSON mode 偶尔返回的空内容也走这条路径:空字符串传给 json.loads 会报错,同样把报错回填后重试。这就是第 2 章“把错误文本回填给模型让它自行纠正”在输出侧的具体做法,只是回填之前多了一道不花钱的机械修复。
业务校验也走同样的路径。platform 的取值不合规时,就明确告诉模型哪里不合规,这比盲目重试三次更有效,也比直接放过去更安全。修正两轮后仍不通过,就转入运营处理流程:把异常连同原始评论一起放进死信队列(专门存放处理失败的消息,等待人工处理或之后批量重跑)。单条脏数据不应该让整条流水线停下来。示例里的业务校验只写了 platform 这一项,sentiment 的三档取值、product 可以为空的约定,可以照着这个样子各补一条判断分支。实际实现时,不要把演示中省略的校验也省掉。
参数方面有两个细节要注意。一是 temperature 要设在 0 附近,格式类任务需要的是输出稳定。二是 max_tokens 必须设得足够大:生成一旦达到长度上限,finish_reason 会变成 length,返回的只是半截 JSON,重试也无法修复。所以要先把上限放宽。
如果端点支持 json_schema,上面这套重试逻辑可以省掉大半。下面换成 Kimi 的端点,看 Schema 级约束怎么写。Kimi 分国内、国际两套端点,两套控制台各自发放密钥,端点地址见官方文档。
python
kimi = OpenAI(
api_key=os.environ["KIMI_API_KEY"],
base_url="<Kimi 国内兼容端点,见官方文档>",
)
# SYSTEM_PROMPT 和 os、OpenAI 的导入沿用上一段;messages 长在 extract()
# 函数内部,这里在模块层重建同款
messages = [
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": "请分析以下评论:\n空气炸锅到手就坏,客服还踢皮球,微博上曝光!"},
]
response = kimi.chat.completions.create(
model="kimi-k3",
messages=messages,
response_format={
"type": "json_schema",
"json_schema": {
"name": "sentiment_record",
"strict": True,
"schema": {
"type": "object",
"properties": {
"platform": {"type": "string", "enum": ["微博", "贴吧", "知乎", "小红书", "B站"]},
"sentiment": {"type": "string", "enum": ["正面", "中性", "负面"]},
"product": {"type": ["string", "null"]},
},
"required": ["platform", "sentiment", "product"],
"additionalProperties": False,
},
},
},
)把 enum 直接写进 Schema,不合规的取值在生成阶段就出不来,前面代码里的业务校验就成了最后一道防线。这道防线要保留。约束的强度取决于厂商的实现,接口行为会随版本变化;保留业务校验,约束一旦失效,能发现问题的就只有它。
各家厂商对 Schema 子集的支持和附加要求不同,OpenAI 对 additionalProperties 的要求、Kimi 的 MFJS 规范都是例子。更换端点时要逐条对照官方文档,不要假定代码原样搬过去就能运行。
字段设计上还有一条通用原则:给模型留一个“答不了”的出口。在输出 Schema 里加一个表示能否解答的布尔字段,再加一个默认为空、专门写无法解答原因的字段,例如命名为 solvable 和 unsolvable_reason。能解答时,把结果填进答案字段;不能解答时,把布尔值设为 false,并在原因字段里写明原因,比如缺少最新信息、没有联网工具。两个字段都设为带默认值的可选项,这样能解答和不能解答两种回复都能用同一个 Schema 表示。
如果不留这个出口,Schema 要求必须填写答案字段,模型无法表示自己答不了,只能编一个答案。格式约束在这时反而起了反作用:它把编造的内容变成了一份合法的 JSON,解析和校验全部通过,下游照常处理。有了这个出口,模型会明确写出这次是否能解答,答不了的样本一眼就能识别,再根据字段决定补工具、转人工还是跳过。
如果任务需要先推理、再输出结构化结果,Schema 会与思维链产生冲突。以客服工单定级为例:模型要先理清纠纷双方的责任,才能填写级别和理由两个字段。推理需要自由地写出中间步骤,而 Schema 约束在采样阶段就把词元限制在格式允许的范围内,中间过程没有地方写。常见的失败有两种。一种是推理压得太短:约束越严格,模型越倾向于尽快把结构填完,中间步骤能省则省。简单任务看不出差别,复杂任务的正确率会最先下降。另一种是推理写得长,写到一半达到长度上限,返回的推理不完整,结论也随之受损。
工程上的解决办法遵循同一个原则:约束只加在最终输出上,不加在推理过程上。最常见的做法是在 Schema 里为推理留一个自由文本字段,把判断过程写在这个字段里,结论另外用单独的字段填写。要求更稳的,可以拆成两次调用:第一次不带 Schema,让模型自由作答;第二次把推理原文连同问题一起发回去,只做信息抽取。这样多一次调用,但推理和格式互不影响。要求不高的场合还有一种更简单的做法:中间内容不限定格式,只对最终输出的字段做校验,格式靠提示词保证。这三种做法目的相同:给推理留出足够的空间,把格式检查放到最后一步。
JSON Schema 在后面还会再用到,只是用途不同。这一节用它约束模型的输出,规定回复的格式;到第 4 章,同一套规范用在工具的参数定义上,规定工具接收什么输入。两边一个约束输出、一个约束输入,学到第 4 章时注意区分。工具调用本身就是结构化输出的一种应用:模型生成的调用请求,就是符合工具 Schema 的结构化输出。这也是本书后面章节的一条主线:输出可靠了,行动才可能可靠。
结构化的格式对输入同样有用。同样的内容,分好章节、标好字段再交给模型,比一整块没有结构的文本更容易理解,模型定位信息更准确,遵循指令也更稳定。长文档放进上下文之前,先花功夫整理一遍结构,在检索和执行两个环节都能受益。反过来,从外部材料里抽取结构化数据时,要先看材料本身的格式:格式稳定的,用正则表达式和模板这类确定性方法最省事,不需要用模型;格式杂乱多变的,才值得交给模型处理,并配上修复回路。这是两件不同的事:输入侧整理结构是要花功夫的,抽取侧要先判断材料的格式,再决定用不用模型。
格式合规只能保证下游代码读得进去,不能保证答案本身正确。
练习:把 SYSTEM_PROMPT 里的格式示例全部删掉,只保留“输出 JSON”一句,连续处理二十条评论,统计解析失败的次数。然后去掉 response_format 参数再跑一遍,比较只靠提示词约束和使用 JSON mode 两种情况下失败率的差距。
3.2 思维链
一件商品标价 249 元,店铺有满 200 减 40 的活动,会员可以在满减后的价格上再打 92 折,运费 8 元不参与优惠。实付多少钱?正确答案是 200.28 元,算法是先满减、再打折、最后加运费。如果把这道题直接发给模型,只要求它给出数字,相当一部分回复是 197.08 元。这个错误很有规律:197.08 是先用 249 乘以 0.92,再减 40,最后加 8 元运费得到的,也就是把“先减后折”的顺序做反了。
每一步单独看都不难,问题出在“直接给数字”这个要求上。不允许写中间过程,模型就只能从题目直接跳到答案。
原因要回到第 1 章讲的生成机制:模型逐个词元生成,每个词元都以前面已经生成的全部词元为条件。要求模型直接作答时,第一步的中间结果没有地方存放,等于要求它一步算出最终结果。把推理过程写出来,情况就不同了:“先减:249-40=209”这一行写进上下文后,下一步“再折:209×0.92=192.28”就以它为条件生成,最终答案又以前面两行为条件。中间结果一旦写进上下文,就成为后续步骤可以直接引用的已知信息,不需要模型在内部自己记住。所以,让模型写出推理过程,相当于让它把中间结果记录下来,供后面的每一步使用。
这种做法的正式名称是思维链,常简写为 CoT,核心是让模型把中间的推理步骤也输出出来,把复杂问题拆成多步来解。2022 年 1 月,Google 的 Jason Wei 等人在论文 Chain-of-Thought Prompting Elicits Reasoning in Large Language Models 中提出了这一方法:在提示词里手写几个推理过程完整的示例,模型会模仿示例,对新问题也逐步写出推理过程。
论文的实验是拿 PaLM-540B 做的,测试集是数学应用题基准 GSM8K,仅用八个示例就达到了当时的最好成绩。论文还指出一个关键现象:这种效果只有在足够大的模型上才会出现,小模型照着做,提升很小,甚至可能变差。
同年 5 月,Kojima 等人在论文 Large Language Models are Zero-Shot Reasoners 中发现,示例也可以完全省掉。只要在问题后面加一句 Let's think step by step,模型就会写出推理过程。仅这一句话就能明显提高正确率,它后来成了业内引用最多的提示语之一。
下面用同一道折扣题做对比:一次直接提问,一次加上思维链引导。
python
import os
from openai import OpenAI
client = OpenAI(
api_key=os.environ["DEEPSEEK_API_KEY"],
base_url="<DeepSeek 兼容端点,见官方文档>",
)
MODEL = "deepseek-v4-flash"
QUESTION = (
"一件商品标价 249 元,店铺活动满 200 减 40,"
"会员在减完活动价的基础上再打 92 折,运费 8 元不参与优惠。实付多少元?"
)
def ask(step_by_step: bool) -> str:
content = QUESTION if not step_by_step else (
QUESTION + "\n先一步一步算,最后单独一行给“答案:<数字>”。"
)
response = client.chat.completions.create(
model=MODEL,
messages=[{"role": "user", "content": content}],
temperature=0,
)
return response.choices[0].message.content
if __name__ == "__main__":
print("直接问:", ask(False))
print("分步问:", ask(True))两种方式各多跑几次,结果有代表性:直接提问的那一路,不时会给出 197.08;分步提问的那一路,稳定得到 200.28。差别就在“先减、再折、最后加运费”这几行推理上。不过现在的旗舰模型普遍内置了思考能力,DeepSeek V4 系列接口默认就开启思考模式,思考过程在服务端进行,与最终答复分开返回。内置思考开启后,上面两种方式可能都能答对,因为分步推理已经在模型内部发生了。
要想复现这个经典的对比效果,需要按各家文档关闭思考功能,或者改用不带思考的型号。DeepSeek 的开关是请求里的 thinking 参数,设为 {"type": "disabled"} 即关闭。它还提供 reasoning_effort 参数,用档位控制思考的投入程度。
多轮对话中,思考内容要不要保留,有一条明确的规则。不带工具的对话,下一轮请求不需要附带历史轮的思考内容,即使附带,服务端也会忽略。但请求一旦带上 tools,规则就相反:历史轮的思考内容必须原样回传。思考内容在响应的 reasoning_content 字段里,回传就是把这个字段随对应的历史消息一起再发回去,漏传会返回 400 错误。
主流旗舰模型几乎都内置了思考能力,这说明一个趋势:过去需要人在提示词里手写的技巧,正在变成模型自带的能力。
2024 年 9 月,OpenAI 发布 o1 预览版。这个模型用强化学习训练,在给出答复之前会先在内部生成一长段思考。reasoning model(推理模型)这个说法从此流行起来。
2025 年 1 月,DeepSeek 发布 R1。论文 DeepSeek-R1: Incentivizing Reasoning Capability in LLMs via Reinforcement Learning 有几条重要结论:仅靠强化学习就能激发出推理能力,不需要人工标注的推理数据;自我检查、回头验证这类行为是在训练过程中自然出现的;权重随后按 MIT 协议开放。
此后,推理能力逐渐成为中文旗舰模型的标配。思考内容和最终答复一样,都是模型生成的词元,需要按输出词元计费,选用这类模型时,这笔开销无法避免。
同一道题,直接提问的费用很低,开启思维链后费用会大幅上升:推理文字全部按输出 token 计费,一道题的输出从几十 token 增加到几千 token 很常见。按第 1 章 1.6 的计费口径,单次调用的费用可能高出几十倍,延迟也相应增加。这笔开销并不是每道题都值得付。思维链能改善的只是多步推理的正确率,对一步就能回答的事实性问题没有帮助;模型本来不知道的事实,推理步骤写得再完整也得不出正确结果,也就是说,它解决不了幻觉问题。
思维链还有一个不太直观的风险:如果某一步出错,后面的步骤会基于这个错误前提继续推下去,最终得到一个推理过程完整、但结论错误的答案。我自己就遇到过:推导写了满满一屏,每一步看起来都通顺,差点直接放行了结论。排查时不能因为推理过程写得详细,就直接相信结论。
还有一个更根本的限制:写出来的推理过程,未必忠实反映模型内部真实的计算过程。研究里管这叫推理的不忠实(unfaithful reasoning)。答案正确时,写出的理由也可能与内部真正起作用的路径不一致;把中间步骤改掉,答案有时也不会变。因此,推理轨迹可以作为排查问题的线索,但不能作为审计凭证。用思维链证明“模型就是这样推理的”,证据的可靠性要打折扣。
思维链把问题分解成子目标时,还会带来一个新问题:拆出来的子目标之间可能互相冲突。订机票就是典型的例子。用户要求“最便宜的直飞”,但最便宜的航班都需要中转,直飞的航班又都不便宜,“最便宜”和“直飞”两个子目标同时成立的方案并不存在。如果智能体识别不出这种冲突,结果只有两种:要么在两个目标之间来回切换,要么悄悄放弃其中一个,用户都不会满意。
处理冲突有固定的顺序。分解目标时,先对照已知的冲突模式检查一遍,比如成本与质量、速度与完整性,这些组合经常互相冲突。发现冲突后,按下面的顺序处理:先看用户的历史记录能否确定优先级,比如这位用户每次都选最便宜的,那么“直飞”就可以降为偏好,不作为硬性条件;确定不了优先级,就放宽约束,比如允许中转一次;仍然不行,就取两个目标各让一步的折中方案;两个目标都不能放弃的,就把代价列清楚,让用户自己决定:最便宜的方案要中转、多花几个小时,直飞要贵多少,用户到底选哪个。
这套处理顺序基于两点判断。第一,冲突本身不是失败,而是一个信号:它说明目标里没有写明优先级,应该回去补充目标,或者直接询问用户。第二,在各种处理方式中,最损害用户信任的是两种不告知用户的做法:一种是悄悄放弃一个目标,比如用户要直飞,却给了中转方案,还不作任何说明;另一种是在两个目标之间反复切换,任务始终无法结束。把冲突明确告诉用户,哪怕只是多问一句,也比这两种做法好。
练习:给折扣题换两组数字,每组各运行五次,分别统计两种方式的正确率。再选一道模型经常算错的多步题,在提示词里放一个带完整推理过程的示例,比较示例引导比一句话引导的效果好多少。
3.3 反思、自洽性与思维树
反思(reflection)指任务做到一半停下来,检查自己的进展。做研究的人在搜索结果与预期不符、需要整合多个来源、或者即将给出结论之前,通常会先停下来问自己:到目前为止发现了什么?方向还对吗?第 2 章的循环缺少的正是这一步:模型每一步只判断“下一步做什么”,不会检查“到目前为止是否顺利”,即使搜索结果明显异常,也不会停下来检查。
2023 年 3 月,Shinn 等人发表论文 Reflexion: Language Agents with Verbal Reinforcement Learning,把这套动作做成了一个框架。它不修改模型权重,而是让智能体把失败的反馈转成一段文字形式的反思,保存到记忆中,下一次尝试时以这段反思为基础。论文把这种做法称为言语强化学习(verbal reinforcement learning)。
Reflexion 还有一个实用的变体,适用于标准答案已知的场景。比如批改作业时已经有标准答案,不要只判断对错,而是让模型对照正确答案分析自己错在哪里,总结出一条下次可以直接使用的经验,存入记忆。同样是一次失败,如果只记录对错,下次多半还会在同一个地方出错;把错误原因和改正方法写成经验保存下来,下一轮才能真正用上。
在工程上,可以把反思做成一个供智能体自查用的工具。reflection 工具只接收两个参数,一段自由文本的分析,以及一个表示“是否需要修改计划”的布尔开关。智能体在四种时机会调用它:完成一个有意义的步骤之后,检查进度;工具报错时,分析原因并寻找替代方案;整合多条互相矛盾的信息时,统一口径;给出最终答案之前,再检查一遍。分析结果写回上下文,后面的步骤就以它为条件。
把这个工具接入第 2 章的回路,只需要改两处:工具清单里增加一项,工具映射里也增加一项。
python
tools.append(
{
"type": "function",
"function": {
"name": "reflection",
"description": "自查进展。完成关键步骤后、工具报错后、信息矛盾时、给出最终答案前调用",
"parameters": {
"type": "object",
"properties": {
"analysis": {
"type": "string",
"description": "对当前进展的分析:卡在哪、为什么、下一步换什么",
},
"change_plan": {"type": "boolean", "description": "要不要改计划"},
},
"required": ["analysis", "change_plan"],
},
},
}
)
def reflection(analysis: str, change_plan: bool) -> str:
return f"已记录自查:{analysis}。{'按新计划继续' if change_plan else '按原计划继续'}"
tool_map["reflection"] = reflection # 工具映射里注册,第 2 章的分发才能找到它注意,这个工具的执行函数几乎什么都不做,只是把内容原样返回。它的作用不在执行本身,而在于模型为了填写这两个参数,必须把自查的内容写成文字。这段文字作为 tool 消息进入消息列表,后面的每一步都以它为条件。工具本身不做判断,判断在模型填写参数时就已经完成了。
分析字段是自由文本,写清楚卡在哪里、原因是什么、下一步换什么方法就够了。例如:“已经搜索了三轮,目标资料还没出现,原因是关键词太宽泛,命中的全是介绍页;下一轮换更具体的关键词,只在官方文档的站内搜索里查找。是否修改计划:是。”思维链把推理过程写成文字,反思把自我评估写成文字,两者的做法是一致的。
反思最有价值的场景是故障恢复。有一个实验把 Wikipedia 工具设置为必然失败:不会反思的智能体一直重试,直到步数用完才停止;会反思的智能体则在中途停下来,写下“此路不通,改用网络搜索”,自行换了方案。
开销方面也要有原则:并不是每次工具调用之后都值得反思一次。在进展顺利的简单任务里强行停下来检查,只会增加没有收益的延迟。第 7 章 Generation 策略里“每完成一个阶段再生成一段反思”的做法,就是把反思用在必要的位置。R1 论文提到自我检查、回头验证这类行为会在强化学习中自然出现,指的也是这类动作。提示词时代的反思技巧,正在逐步变成推理模型自带的能力。
写出来的推理不仅可以用来自我检查,还可以用来投票。自洽性(self-consistency)走的是后一条路:温度不要设得太低,让模型对同一道题生成多组不同的推理和答案,哪个答案出现得最多,就采用哪个。比如三个解里有两个相同,就取这两个相同的答案。这个方法搬到多智能体场景,就是让几个智能体各自独立回答同一道题,再按多数结果确定答案。成本会相应增加,从几次采样变成几次完整的智能体运行,只有真正重要的决策才值得使用。
投票的代码是一个循环加一次计数。client 和 MODEL 沿用 3.2 的定义。
python
from collections import Counter
def ask_with_vote(question: str, n: int = 5) -> str:
answers = []
for _ in range(n):
resp = client.chat.completions.create(
model=MODEL,
messages=[{"role": "user", "content": question + "\n分步算,最后单独一行给“答案:<数字>”。"}],
temperature=0.8, # 温度别压太低,采样要的就是不一样
)
text = resp.choices[0].message.content
answers.append(text.split("答案:")[-1].strip())
top, votes = Counter(answers).most_common(1)[0]
print(answers, "→", top, f"({votes}/{n} 票)")
return top五个解里有三个相同,这个占多数的答案,就比只采样一次得到的单个答案更可靠。原因在采样结果的分布上:答对的路径多半落到同一个答案上,票数集中;答错的路径各走各的,票数分散,凑不成多数。多数票因此聚在正确的一边。
评估答案是否一致,也不一定要数票。另一种通行的做法是把每个答案转换成向量,计算答案之间的距离,选出离所有答案平均位置最近的那个。文字怎么转换成向量,第 6 章讲嵌入时再展开。不过这个方法也有失效的情况。有一道时间旅行难题,正确答案是 26 天,不同模型给出的答案分散在 6 天、13 天和 27 天,同一个模型问两次也可能给出不同的数,始终得不到 26 天。投票有效的前提是答对的那几次采样得出相同的答案。题目较简单时,多数推理路径都通向正确答案,才能形成多数;题目真正很难时,模型一直在出错,而且错法各不相同,即使最集中的那个答案,仍然是错的。所以这个方法对难度中等偏低的题目帮助更明显,对真正超出模型能力的难题没有作用。
自洽性还有一个成本更高的扩展,叫思维树(tree of thought,缩写 ToT)。做法是把求解过程拆成一步一步的链,每一步先生成多个候选,每个候选各评估一次,评估不合格的直接丢弃,不再往下推进。自我评估因此嵌入了链上的每个节点,前面讲的反思动作,在这里成为每一步都要执行的例行检查。
以一棵三层的树为例,算一下调用次数。每个节点先生成 3 个候选,各评估一次,只保留评分最高的一个继续往下走,其余全部剪掉。这样每一层需要 3 次生成加 3 次评估,共 6 次调用,三层合计 18 次。而直接把问题问模型一遍,只需要 1 次调用。再看不剪枝、把所有分支全部展开的情况:第一层 3 个候选,第二层 9 个,第三层 27 个,生成和评估合计 78 次调用;剪枝把调用次数降到了 18 次。层数继续增加,或者每层多保留几个候选,调用次数会按指数增长,一道题用掉几十次调用并不少见。
自洽性和思维树什么时候用?推理模型已经在单次调用内部做了较充分的思考,日常任务用不到。重要的决策值得靠自洽性多采几个样;真正困难、值得花几十次调用的问题,思维树仍然有用。
思维树的论文给出过具体的数字对比。它用 Game of 24 做测试:给出四个数字,用加减乘除凑出 24。GPT-4 用思维链只解出 4%,改用思维树后解出 74%。这类题需要提前规划,走错了还要退回去换一条路,正是单链推理不擅长的类型。差距这么大,多花几十次调用就是值得的。
这一类方法有个统一的名称,叫测试时计算(test-time compute)。思路是不增大模型,而是在推理阶段多投入计算:多采样几个解,多改进几次,定稿之前先比较一遍。
用这个框架回头看,前面两种方法正好各占一个方向。自洽性是横向扩展:对同一道题并行采样多个相互独立的解。反思是纵向深入:针对一个解反复修改。还可以按模型档位分工:生成候选这种量大的工作交给便宜的小模型,最终评分和裁决交给旗舰模型,两档搭配可以明显降低费用。
思维树把这两种思路都放进了链上的节点。把整套搜索过程整理成正式算法的,是蒙特卡洛树搜索(Monte Carlo Tree Search,简称 MCTS)。它每一轮分四步:选择,从根节点开始,选出当前最值得探索的节点;扩展,在这个节点上增加一个新分支;模拟,沿新分支快速走到出结果为止,估计这条路径的好坏,走法很简单,常常是随机选择,只求快、不求精;回传,把结果沿原路径写回途经的每个节点。选择哪个节点,由置信上界(UCT)公式决定:平均成绩高的节点得分高,探索次数少的节点得分也高。前者让已经证明有效的路径继续得到探索,后者给尚未尝试的路径留出机会,搜索就在这两者之间自动取得平衡。
工程实践中很少需要自己实现 MCTS,日常接触到的都是它的产品化形态。旗舰模型的内置思考和 reasoning_effort 分档,本质上都是厂商在替用户分配推理阶段的计算资源。真正值得自己实现树搜索的情况只有一种:题目难到值得花几十次调用,同时又没有现成的推理模型可用。推理能力还有另一条来源,就是在训练阶段把多步推理直接训练进模型权重,这是模型厂商的工作,使用者能做的只是选型时挑选推理档位,这里不展开。
推理的方向也不只有从前提推出结论这一种。用于诊断排障的智能体常用另一种方式,学术上叫溯因(abductive reasoning):先观察到结果或症状,再倒推最能解释它的原因,然后通过一次检查来验证这个推测。第 14 章的失败分析、运维场景的根因定位,基本上都是这种做法。与之配套的还有对自身状态的判断:知道自己已经掌握什么、还缺什么、进行到了哪一步,第 14 章讲元认知时专门展开。
多步任务执行到后期,还有一个目标漂移的问题要防范:前面积累的工具返回内容会稀释开头交代的目标,到后半程,模型逐渐偏离最初的目标。对策是给任务使命、交付物和硬性约束设定一个固定格式,每个大阶段开始之前,先把这些内容重新引用一遍再继续。配套的做法是控制规则清单的长度,条目太多时,后面新增的规则会稀释早期的规则,需要定期回头合并、删减。
推理过程一旦写出来,人可以把它当作调试材料,智能体可以拿它当反思的依据。
练习:在第 2 章的回路里加入一个 reflection 工具,参数只保留自由文本的分析和“是否修改计划”的开关。再把 query_express 改成必然失败,分别运行带反思和不带反思的两个回路,观察它们各自在哪里停下来。
3.4 ReAct 循环
模型想一步,代码做一步,列表记一步,这套节奏有正式的名称 ReAct。2022 年 10 月,Yao 等人在论文 ReAct: Synergizing Reasoning and Acting in Language Models 中提出了这种模式,核心是让推理和行动交替进行:推理决定行动的方向,行动从外部环境取回真实信息,再作为下一步推理的输入,论文摘要把这称为两者之间的协同。
这篇论文发表的时间,比 OpenAI 在 Chat API 中加入函数调用早了八个月。在没有原生机制的那段时间,要让模型使用工具,唯一的办法是把调用规则约定成一套文本协议。
这套协议是什么样的?可以看从论文沿用至今的格式约定。字段一共六个:Question、Thought、Action、Action Input、Observation 和 Final Answer。工具的说明清单和名字清单都写在提示词里,模型按格式逐行写:Thought 写这一步的判断,Action 填工具名,Action Input 填参数,Observation 这一栏留给代码补充。工具执行完毕后,代码把结果拼接到提示词末尾,再请求一轮,模型接着写,信息足够时用 Final Answer 结束。
整个过程就是一段不断变长的文本,ReAct 智能体的全部状态都保存在这段文本里。第 2 章循环中的解析、回填、续写,在这里都变成了对这段文本的拼接操作。
把 ReAct 的六个字段和第 2 章函数调用回路放在一起,两代实现的对应关系一眼就看出来。
| ReAct 字段 | 函数调用回路中的对应物 | 谁来写 |
|---|---|---|
| Question | user 消息 | 用户 |
| Thought | assistant 消息的推理段 | 模型 |
| Action | tool_calls 里的函数名 | 模型 |
| Action Input | tool_calls 里的参数 JSON | 模型 |
| Observation | tool 消息的执行结果 | 代码 |
| Final Answer | 收尾的 assistant 消息 | 模型 |
字段一一对应,区别只在载体:函数调用把结构交给 API 的参数字段承载,ReAct 把结构压进提示词的文本格式。第 2 章手写的那个循环,就是这条骨架换上函数调用载体后的样子。
只推理不行动时,模型只能依靠参数里已有的知识,遇到不知道的事实就会编造。只行动不推理时,模型会连续调用工具,但缺少“这样做是否正确”的检查。两者交替进行,在多跳问答(需要连续查询多次才能回答的问题)和决策类任务上,效果明显好于只用其中一种。这个结论到 2026 年仍然成立,只是实现方式已经更新了一代。
下面把这套协议写成一个能运行的最小循环,看看没有原生函数调用时是怎么实现的。工具仍然取自客服场景,解析用正则表达式,不使用任何框架。
python
import json
import os
import re
from openai import OpenAI
client = OpenAI(
api_key=os.environ["DEEPSEEK_API_KEY"],
base_url="<DeepSeek 兼容端点,见官方文档>",
)
MODEL = "deepseek-v4-flash"
def query_order(order_id: str) -> str:
"""查询订单的发货状态。参数 order_id 是订单号。"""
db = {
"A20260824001": "已发货,顺丰 SF8811,昨日 21:00 揽收",
"A20260824002": "未发货,当前地址:北京市朝阳区望京街道 88 号",
}
return db.get(order_id, f"订单 {order_id} 不存在")
def update_address(order_id: str, new_address: str) -> str:
"""修改订单收货地址。参数 order_id 是订单号,new_address 是完整新地址。"""
return f"订单 {order_id} 的地址已改为 {new_address}"
tool_map = {"query_order": query_order, "update_address": update_address}
PROMPT_TEMPLATE = """你在客服系统里处理订单问题,可以调用下面的工具:
{tool_desc}
严格按以下格式回复,每次只走一步:
Thought: 这一步的判断
Action: 工具名(只能是 {tool_names} 之一)
Action Input: JSON 格式的参数
拿到足够信息后,改用这个格式收尾:
Thought: 已经可以回答用户
Final Answer: 给用户的最终答复
示例:
Thought: 先查两个订单哪个没发货
Action: query_order
Action Input: {{"order_id": "A20260824001"}}
现在处理这个问题:
Question: {question}
Thought:"""
def run_react(question: str, max_steps: int = 8) -> str:
prompt = PROMPT_TEMPLATE.format(
tool_desc="\n".join(
f"- {name}: {fn.__doc__}" for name, fn in tool_map.items()
),
tool_names=", ".join(tool_map),
question=question,
)
for step in range(max_steps):
response = client.chat.completions.create(
model=MODEL,
messages=[{"role": "user", "content": prompt}],
stop=["Observation:"],
temperature=0,
extra_body={"thinking": {"type": "disabled"}}, # 文本协议要裸补全,关掉服务端思考
)
text = response.choices[0].message.content
print(f"=== 第 {step + 1} 步 ===\n{text}\n")
if "Final Answer:" in text:
return text.split("Final Answer:")[1].strip()
match = re.search(
r"Action:\s*(\w+)\s*\nAction Input:\s*(\{.*\})", text, re.S
)
if match is None:
# 格式不合约定,把纠正要求当作观察回填
prompt += text + "\nObservation: 无法解析你的回复,请严格按格式重发\nThought:"
continue
action, raw_args = match.group(1), match.group(2)
try:
result = tool_map[action](**json.loads(raw_args))
except (KeyError, TypeError, json.JSONDecodeError) as e:
# 工具名不在清单、参数名对不上、参数不是合法 JSON,同样当观察回填让模型自纠
result = f"执行失败:{e},请检查工具名与参数格式后重发"
prompt += text + f"\nObservation: {result}\nThought:"
return "步数用尽,任务未完成。"
if __name__ == "__main__":
print(run_react(
"订单 A20260824001 和 A20260824002 哪个还没发货?"
"把没发货那个的地址改成上海市杨浦区五角场街道 100 号。"
))看一次典型的运行过程:第一步查第一个订单,观察返回“已发货”;第二步查第二个订单,观察返回“未发货”;第三步修改地址;第四步用 Final Answer 结束。如果把问题里的订单号换成库里不存在的,错误同样通过观察传回给模型。query_order 只返回一句“订单不存在”,模型读到后就如实调整答复并结束,不会再执行第三步改地址。错误不需要单独的反馈通道,模型读取观察时就已经据此调整了。
论文发表的时候还没有 Chat API,整套协议是用纯文本发给补全接口的。这里为了简便,把文本放进一条 user 消息发送,协议逻辑没有变化。思考模式已经通过 extra_body 在请求里关闭;也可以直接选用不带思考的型号,效果相同。
代码里有三处设计,是文本协议时代的做法。第一处是 stop=["Observation:"]。模型写完 Action Input 之后经常不会停下来,而是继续把 Observation 也写出来,一段凭空编出来的“查询结果”就进入了上下文,后面每一步都会出错。stop 参数的作用是让服务端一生成到这个词就截断输出,保证观察内容只来自真实的工具执行。
第二处是正则解析。格式约定得再严格,模型仍然可能偏离格式。解析失败时不让程序崩溃,而是把“无法解析”作为一条观察回填给模型,让它自己修正格式,思路和 3.1 的修复回路相同。防护也不只有正则这一道:Action Input 不是合法 JSON、工具名不在清单里、参数名与函数定义不符时,try 会捕获异常,同样按观察回填处理。
第三处是不断变长的 prompt。每一步都要把模型上一步的输出和真实观察拼接进去,再整体重新发送。上下文变长后,模型对早期内容的把握会变弱。第 2 章 messages 列表遇到的上下文膨胀、何时裁剪、注意力稀释这些问题,在这段文本上同样存在,而且出现得更早。文本协议和后来的原生函数调用之间的差别,主要就体现在这三处细节上。
原生函数调用普及之后,这套文本协议还值得学吗?可以分三层来看。Action 这一层确实可以不用了:第 2 章的 tool_calls 把工具名和参数都变成了结构化字段,正则解析和 stop 截断都不再需要,可靠性也更高。能使用原生函数调用的场合,就用原生的。
Action 这一层还有一个不明显但影响很大的差别。文本协议每一步只允许一个 Action,两件互不相关的事也要拆成两轮,而每一轮都是一次携带全部历史的完整请求。原生接口的 tool_calls 本身就是一个列表,模型一轮就可以同时发出多个调用。第 2 章的代码用 for 循环遍历 message.tool_calls,就是为了处理多个调用。文本协议在串行和并行上的这个限制,到了生产环境,就直接变成延迟和费用上的差别。
Thought 这一层不仅没有过时,反而越来越重要。3.2 讲过,推理模型把显式推理直接训练进了模型内部;调试排错时,查看的正是每一步的 Thought,它也是轨迹记录中最重要的内容。骨架这一层从来没有变过:想一步、做一步、记一步的循环,到 2026 年仍然是各家智能体框架的基础,变化的只是每一步里包含的内容。
接口层面也在把 Thought 变成正式的组成部分。有厂商已经支持在多轮工具调用之间保留思考状态,上一轮还没有完成的判断不用重新交代;更进一步的形态是,工具还在执行时,模型就能收到新消息并立即调整计划,不用等这一轮结束。这两项能力各家的进度不同,使用前要查阅当期文档,确认自己用的模型支持到什么程度。
这套文本协议用起来有三类常见的失效方式,每一类都有对应的护栏。
第一类,格式漂移。模型偶尔会在 Thought 和 Action 之间插一段解释文字,或者把 Action Input 的 JSON 少个引号,正则解析当场就断了。护栏有两条:解析失败时把报错原文回填给模型,让它自己改一遍再发,这比直接崩掉多一次调用,但大多数都能救回来;关键业务上改用结构化输出约束格式,3.1 的 Schema 在这里正好用得上。
第二类,死循环。模型对同一个工具连发同样的调用,Thought 里写的话每轮都一样,不做任何新的判断。护栏是记录最近几次的调用签名,发现重复就把这个事实回填给模型,提醒它换条路走,再配一个轮数上限兜底。
第三类,推理链断裂。Thought 写了结论但没写依据,或者中间跳了一步,后面全沿着错的走。护栏是在关键节点要求 Thought 写出证据,第 9 章会展开这种把推理过程固定下来的做法。
想看两种实现的对照,可以看智能体框架 LlamaIndex:OpenAIAgent 使用原生函数调用,ReActAgent 使用提示词文本协议。后者至今仍在使用,是因为它可以驱动任何模型,即使模型本身不支持函数调用。另一个常用框架 LangChain 中的 create_react_agent,沿用的也是论文传下来的那套提示词模板。遇到三类情形时,这套文本协议仍然是最直接的选择:手上的模型不支持函数调用;需要对决策过程做最细致的控制;阅读框架源码时需要理解它的来龙去脉。
练习:往 tool_map 里增加一个 refund_order(order_id) 工具,观察模型如何安排三件事的先后顺序。去掉 stop 参数连续运行几遍,统计模型自己编出 Observation 的次数。最后把 max_steps 改成 2,观察任务在哪一步中断。
本章小结
- 3.1 结构化输出:约束分三层,提示词示例、JSON mode 和 JSON Schema,一层比一层强;修复走阶梯,先机械修复,修不动把报错回填给模型重试,业务校验再拦一道,两轮不过进死信队列;Schema 里给模型留一个“答不了”的出口;约束只加在最终输出上,不加在推理过程上。
- 3.2 思维链:让模型把中间推理写出来,正确率的收益限于多步推理题;思考内容按输出 token 计费,单次调用费用可能高出几十倍;写出的推理未必忠实,只能当排查线索,不能当审计凭证。
- 3.3 反思、自洽性、思维树:反思实现成一个自查工具,完成关键步骤、工具报错、信息矛盾和给出答案前这四种时机调用;自洽性靠多采样投票,对难度中等偏低的题目帮助明显;思维树逐节点评估再剪枝,三层三叉要 18 次调用,值得花几十次调用的难题才用。
- 3.4 ReAct:六个字段 Question、Thought、Action、Action Input、Observation 和 Final Answer,全部状态保存在一段不断变长的文本里;格式漂移、死循环和推理链断裂三类失效各有护栏;函数调用是这套骨架在今天的实现形式。
参考资料
DeepSeek API 文档:Reasoning Model(思考模式输出 reasoning_content、多轮对话历史轮的 reasoning_content 应回传、reasoning_effort 档位)https://api-docs.deepseek.com/ ,核实于 2026-10-07
DeepSeek-R1 论文《Incentivizing Reasoning Capability in LLMs via Reinforcement Learning》(纯强化学习激发推理能力的实验与结论)https://arxiv.org/abs/2501.12948 ,核实于 2026-10-07
《Designing AI Agents》(Jia Huang,Manning MEAP)第 5 章(思维链的不忠实性:写出的推理未必反映真实计算过程,正确答案可配错误理由),见 txt/designing-ai-agents.txt(英文原文)
《Agentic GraphRAG》(Anthony Alcaraz,O'Reilly 2026 中译)第 5 章(交错思考:工具调用间保留思考状态、执行中途转向的接口演进),见 txt-ref-zh/agentic-graphrag/
《Agentic Architectural Patterns for Building Multi-Agent Systems》(Ali Arsanjani、Juan Pablo Bustos,Packt 2026-01)第 8 章(指令契约回引:使命/交付物/硬约束固定格式、阶段开头回引防目标漂移),见 txt-ref-zh/agentic-architectural-patterns/
《构建 Agentic AI 系统》(Anjanava Biswas、Wrick Talukdar 著,茹炳晟、殷海英译,清华大学出版社 2025)第 3 章(溯因推理:从观察反推最佳解释的推理式),见 txt/构建AgenticAI系统.txt
《AI Agents: The Definitive Guide》(Nicole Königstein,O'Reilly 中译)第 3 章(混合专家架构的参数账:总参数定显存、激活量定算力;思维树的模型档位分工),见 txt-ref-zh/ai-agents-definitive-guide/
Wei J. 等:Chain-of-Thought Prompting Elicits Reasoning in Large Language Models(arXiv:2201.11903,2022-01 提交)https://arxiv.org/abs/2201.11903 核实于 2026-08
Kojima T. 等:Large Language Models are Zero-Shot Reasoners(arXiv:2205.11916,2022-05 提交)https://arxiv.org/abs/2205.11916 核实于 2026-08
Yao S. 等:ReAct: Synergizing Reasoning and Acting in Language Models(arXiv:2210.03629,ICLR 2023)https://arxiv.org/abs/2210.03629 与官方仓库 https://github.com/ysymyth/ReAct 核实于 2026-08
Yao S. 等:Tree of Thoughts: Deliberate Problem Solving with Large Language Models(arXiv:2305.10601,2023;GPT-4 解 Game of 24 思维树 74% 对思维链 4% 的出处)https://arxiv.org/abs/2305.10601 核实于 2026-09-01
Shinn N. 等:Reflexion: Language Agents with Verbal Reinforcement Learning(arXiv:2303.11366,NeurIPS 2023)https://arxiv.org/abs/2303.11366 核实于 2026-08
OpenAI:Introducing Structured Outputs in the API(2024-08)https://openai.com/index/introducing-structured-outputs-in-the-api/ 及 Structured outputs 文档 https://developers.openai.com/api/docs/guides/structured-outputs 核实于 2026-08
MoonshotAI walle 仓库(MFJS 规范文档与开源校验器:Moonshot Flavored JSON Schema 是一份精选的 JSON Schema 子集)https://github.com/MoonshotAI/walle 核实于 2026-09-02
OpenAI:Introducing OpenAI o1 https://openai.com/index/introducing-openai-o1-preview/ 及配套研究博文 Learning to reason with LLMs(o1 用强化学习训练、作答前先生成内部思考链的出处)https://openai.com/index/learning-to-reason-with-llms/ 两篇均核实于 2026-08-29
《Designing Multi-Agent Systems》第 4 章(结构化输出与工具调用的关系),见 txt-ref-zh/designing-multi-agent-systems/
《AI Agents in Action》第 2 版第 5 章(时间旅行难题:正确答案 26 天,不同模型给出的答案散落在 6、13、27 天上,同一个模型两次作答也可能不同),见 txt-ref-zh/ai-agents-in-action-2e/
《Build an AI Agent (From Scratch)》第 2 章(GAIA 输出模式的可解性出口:is_solvable、unsolvable_reason、final_answer 三字段与三分支提示词)、第 4 章(不带搜索的智能体要么返回 is_solvable=False 要么幻觉作答的失败样貌)、第 7 章(反思的四类时机、reflection 工具实现、故障恢复实验),见 txt-ref-zh/build-ai-agent-from-scratch/
《AI Agents and Applications with LangChain, LangGraph and MCP》第 2 章(ToT 帮 GPT-4 解出 74% 的 Game of 24 题目、CoT 仅 4% 的数字链出处,原引 arXiv:2305.10601),Roberto Infante,Manning 2026,译文见 txt-ref-zh/agents-langchain-langgraph-mcp/
《AI Agents: The Definitive Guide》第 3 章(测试时计算框架、广度采样与深度精修两族、MCTS 与 UCT;正文这一族做法的说法与分类整理自该章,小模型靠更多推理时间追平大模型一类研究结论为该书转述,引用请照该书标注),见 txt-ref-zh/ai-agents-definitive-guide/
《AI Agent 开发实战》第 3 章(ReAct 双实现:LlamaIndex OpenAIAgent 与 ReActAgent、LangChain create_react_agent),见 txt-ref/ai-agent开发实战/
《AI Agents in Action》第 10 章(推理提示技术谱系、自洽性嵌入均值评估、ToT 三路分叉流与 27 次调用上限;正文 3.3 的三层三叉 18 次调用为本书自造的可验算示例,非该书数字),见 txt-ref-zh/ai-agents-in-action/
Dhivya Nagasubramanian《Agentic AI for Engineers》(Apress/Springer 2026)第 4 章(子目标冲突消解:分解时查已知冲突模式,按用户历史定优先级、放松约束、折中、摆明代价请用户仲裁;冲突不是失败是信息;静默丢目标与来回打转最伤信任),见 txt/agentic-ai-for-engineers.txt(英文原文,第 4 章,行 4852-4880、5464-5469)
Max Gfeller《Building Agentic Applications with CrewAI and MCP》(Manning,MEAP)第 2 章(结构化输出校验失败的修复阶梯:先机械修复多余逗号、引号与未闭合花括号,修不动再回填报错重试,省一次模型调用),见 txt/crewai-mcp.txt(英文原文,第 2 章,行 1048-1057)