过去几年里,Agent 逐渐成为一个越来越难绕开的词。但什么是 LLM Agent,Agent 是怎么工作的,从工程角度又该如何构建一个 Agent 应用?我想通过这篇文章来稍微理解这些东西。
ReAct
如果说 Agent 的关键是自主决定下一步,那么 ReAct 讨论的就是这种决定如何发生:让模型在推理和行动之间交替进行,并通过工具反馈更新后续决策。
ReAct 是 Reasoning + Acting 的缩写。它最早由 Shunyu Yao 等人在论文 ReAct: Synergizing Reasoning and Acting in Language Models 中系统提出。论文给出的核心定义是:让 LLM “generate both reasoning traces and task-specific actions in an interleaved manner”,也就是让模型交替生成推理轨迹和任务相关行动。
它想解决的是两个单独范式各自的缺陷。
第一,只有推理不够。Chain-of-Thought 可以让模型把中间思考写出来,但它主要依赖模型内部知识。遇到需要实时信息、外部事实、数据库状态、网页环境的问题时,模型没有办法自己验证,也容易把错误一路传播下去。
第二,只有行动也不够。如果模型只是不断选择动作,比如搜索、点击、调用 API,它可能缺少一个显式的工作记忆和计划过程:为什么现在要调用这个工具?上一步观察说明了什么?下一步该查哪个实体?出了错要不要换策略?
ReAct 的做法很简单:把 Agent 的动作空间扩展成两类东西。
- 一类是对外部世界有影响的 Action,比如
Search[Apple Remote]、Lookup[Front Row]、Finish[answer]。 - 另一类是语言形式的 Thought,也就是推理轨迹。Thought 本身不改变外部环境,但会进入上下文,帮助后续推理或行动。
一个典型的 ReAct 轨迹如下:
Question: ...
Thought 1: 我需要先查 A 和 B 的关系。
Action 1: Search[A]
Observation 1: ...
Thought 2: 搜索结果提到了 B,但还需要验证 C。
Action 2: Lookup[C]
Observation 2: ...
Thought 3: 现在信息足够了。
Action 3: Finish[final answer]
这里的重点不是这些字段名,而是循环结构:
推理 -> 行动 -> 观察 -> 再推理 -> 再行动 -> ...
论文里把这个关系说得很清楚:reasoning traces 帮模型创建、维护和调整行动计划,也能处理异常;actions 则让模型接入外部知识源或环境,把新的信息带回推理过程。换句话说,Thought 负责“决定下一步为什么这样做”,Action 负责“去外部世界拿证据或产生影响”,Observation 负责“把世界的反馈写回上下文”。
这也是为什么 ReAct 对 Agent 框架影响很大。一个 Agent 不再只是:
用户输入 -> LLM -> 最终回答
而变成:
用户输入
|
LLM 生成下一步决策
|
如果需要工具:执行 Tool
|
把 Tool 结果作为 Observation 放回上下文
|
再次调用 LLM
|
直到输出最终答案
LangChain 的 Agent 文档里就能看到这个范式的工程化版本。LangChain 的核心 Agent 实现直接沿用了 ReAct 的命名和结构,内部构建了一个"模型决策 → 工具执行 → 结果反馈"的循环:模型判断是否需要调用工具以及用什么参数,runtime 执行工具并把结果放回上下文,模型再次决策,直到给出最终答案或达到循环上限。
在具体消息结构里,这个循环大概对应成:
HumanMessage
-> AIMessage(tool_calls=[...])
-> ToolMessage(content="工具返回结果")
-> AIMessage(tool_calls=[...] 或 final answer)
也就是说,早期 ReAct prompt 里显式写出来的 Thought / Action / Observation,在现代工具调用模型和 Agent runtime 里不一定都以文本字段出现。Action 变成结构化的 tool_calls,Observation 变成 ToolMessage,而 Thought 有时由模型内部完成(不体现在输出中),有时作为模型返回消息的文本部分输出,也可以通过 trace 工具或开发者在 prompt 中要求模型显式推理来暴露。
所以理解 ReAct 时,最好不要把它看成固定 prompt 模板,而要看成一种 Agent 控制流范式:
- LLM 不是一次性回答,而是反复决定下一步。
- Tool 不是附属功能,而是 Action 的执行载体。
- Observation 不是普通文本,而是下一轮决策的输入。
- Agent runtime 的价值,是管理这个循环、状态、停止条件、错误处理和可观测性。
这也解释了为什么 LangChain 后面需要 Tool、Runnable、Agent、LangGraph 这些抽象:ReAct 把“模型会调用工具”这件事变成了一个循环,而框架要负责把这个循环稳定地跑起来。
需要注意,ReAct 不是所有 Agent 的唯一形态。确定性的 workflow、plan-and-execute、多 Agent 协作、纯 RAG chain 都不一定是 ReAct。但在今天很多“LLM + tools”的 Agent 里,ReAct 仍然是最基础、最容易理解的一种范式:模型边想边做,工具把外部世界带回上下文,直到任务结束。
Agent 构建范式
ReAct 很重要,但它不是 Agent 的全部。更准确地说,ReAct 是“单 Agent + 工具 + 推理/行动交替”这一支的代表形态。围绕“模型怎样获得外部信息、怎样决定下一步、怎样和其他 agent 协作”,Agent 逐渐形成了几类常见构建方式。
先看一张关系图:
LLM Agent 构建范式
│
├─ RAG / Agentic RAG
│ └─ 把外部知识库接入模型;agentic 版本由模型决定何时检索
│
├─ Tool-using Agent(MRKL 是早期代表)
│ └─ ReAct:在工具调用基础上交替进行推理、行动、观察
│
├─ Planning Agent:Plan-and-Execute / ReWOO
│ └─ 先形成任务计划,再执行步骤;或把推理计划和工具观察解耦
│
├─ Reflection Agent:Reflexion / Evaluator-Optimizer
│ └─ 执行后根据反馈评价、修正、重试,必要时写入记忆
│
└─ Multi-agent Orchestration:多 Agent 协作编排
└─ 多个 agent 分工、对话、转交和汇总
这几类不是互斥关系,而是经常组合在一起。例如,一个研究助手可能同时是 RAG、ReAct、Plan-and-Execute 和多 Agent;一个代码助手可能是 ReAct + 工具调用 + evaluator 反馈循环。
下面分别说明这些范式的来源和核心思路。
第一,MRKL 是工具增强 Agent 的早期系统化表达,也是 Tool-using Agent 这条路线的重要代表。《MRKL Systems》把系统拆成语言模型和一组外部知识、推理、计算模块,核心思想是:不要让 LLM 独自承担所有知识和推理,应该让它路由到合适的专家模块。现在的 tool calling、function calling、MCP、LangChain Tool,本质上都可以看成这条路线的工程化延续。
第二,ReAct 在工具调用之上加入了显式的推理-行动循环。MRKL 更强调“LLM 如何调用外部模块”,ReAct 更强调“每一步行动前后如何用语言推理维护任务状态”。因此 ReAct 可以理解成:
Chain-of-Thought 的推理轨迹
+
MRKL / Tool use 的外部行动能力
=
ReAct 的 Thought -> Action -> Observation 循环
第三,Plan-and-Execute、ReWOO 是对 ReAct 循环粒度的改造。ReAct 通常每一步都让大模型看完整上下文,然后决定下一个 action;这很灵活,但 token 成本高,也容易一步一步漂移。Plan-and-Execute 先生成全局计划,再逐项执行;LangChain 的 planning agents 介绍也明确把它作为区别于 ReAct 的 agent 架构。ReWOO 则更进一步,把“推理计划”和“工具观察”解耦,先生成带变量引用的工具调用计划,再集中执行和综合。
第四,Reflexion 不是替代 ReAct,而是给任意 Actor 外面套一层反馈学习机制。Reflexion的关键点是不更新模型参数,而是让 agent 根据外部反馈或自评生成 verbal reflection,再把这些反思放进 episodic memory,影响下一次尝试。常见结构是:
Actor(可以是 ReAct / Plan-and-Execute / 普通工具 Agent)
-> 执行任务
-> Evaluator 给反馈
-> Self-reflection 生成反思
-> Memory 保存经验
-> 下一轮 Actor 使用经验重试
第五,RAG 本身不一定是 Agent,但 Agentic RAG 是很常见的 Agent 子形态。普通 RAG 是“先检索,再生成”的固定链路;Agentic RAG 则把检索器、数据库、文件搜索、网页搜索做成工具,由模型判断什么时候查、查什么、是否需要再次检索。它的论文根基是 Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks,工程实现常见于 LlamaIndex、LangChain、Haystack 这类框架里。
第六,多 Agent 不是“更高级的 ReAct”,而是组织方式升级。单个 agent 可以是 ReAct、Plan-and-Execute 或 Reflexion;多 Agent 关心的是“多个 agent 怎么分工、通信、转交和合并结果”。AutoGen把这个方向定义为多个 conversable agents 通过对话协作完成任务,agent 可以组合 LLM、人类输入和工具。OpenAI Agents SDK 的多 Agent 文档也把工程分类讲得很清楚:常见两种模式是 Agents as tools 和 Handoffs;前者是 manager 保持控制、把专家 agent 当工具调用,后者是 triage agent 把当前对话控制权交给专家 agent。
下面列出当前较典型的 Agent 构建方式:
| 范式 | 核心问题 | 代表实现 | 适合场景 |
|---|---|---|---|
| RAG / Agentic RAG | 外部知识怎么进入模型上下文 | LlamaIndex、LangChain、Haystack | 企业知识库、文档问答、需要事实依据的检索问答 |
| Tool-using / MRKL | 模型如何调用外部模块、API、代码和环境 | LangChain、OpenAI Agents SDK | 搜索、计算、数据库查询、API 操作、代码执行 |
| ReAct | 每一步如何在推理、行动、观察之间循环 | LangChain / LangGraph、OpenAI Agents SDK | 开放式工具调用、多步问答、网页/代码/数据分析 |
| Plan-and-Execute / ReWOO | 长任务如何先规划、再执行,减少逐步漂移和重复调用 | LangGraph(教程/参考实现) | 步骤明显的长任务、研究任务、自动化任务 |
| Reflexion / Evaluator-Optimizer | 如何根据反馈修正结果、从失败中改进 | 通常基于 LangGraph 等编排层自行构建 | 代码修复、写作润色、搜索迭代、有明确评价标准的任务 |
| Multi-agent | 多个 agent 如何分工、转交、并行和汇总 | AutoGen、CrewAI、LangGraph、OpenAI Agents SDK | 角色明显、上下文可隔离、子任务可并行的复杂任务 |
实际落地时,通常不是只选一种,而是把几种模式组合起来:
企业知识问答:
RAG / Agentic RAG + Tool-using Agent
代码/数据分析助手:
ReAct + Tool-using Agent + Reflexion / Evaluator-Optimizer
复杂研究任务:
Plan-and-Execute + RAG / Agentic RAG + Multi-agent Orchestration
客服/业务流程:
Multi-agent Orchestration + Tool-using Agent
长任务自动化:
Plan-and-Execute + ReAct + Reflexion / Evaluator-Optimizer
更准确地说:Agent 构建范式不是从 ReAct 往“更多 agent”单线升级,而是在三个维度上组合:
- 行动维度:不用工具、用检索、用 API、用代码执行、用浏览器/电脑。
- 控制维度:ReAct 循环、先规划后执行、反馈迭代。
- 组织维度:单 agent、manager + workers、handoff、multi-agent orchestration。
LLM应用框架
LangChain 本质上是一个用于构建 LLM 应用的工程框架。
LangChain 主要做两件事:
- 提供模型调用的标准化接口,不局限于具体的 LLM
- 把各类能力组织成可组合的流程
LangGraph、LlamaIndex 也是 LLM 应用框架。LangGraph 更偏编排,LlamaIndex 更偏检索与 RAG。
LangChain 对不同能力做了抽象:
- 模型抽象(chat/completion/embedding)
- 提示词抽象(PromptTemplate)
- 工具抽象(Tool)
- 检索抽象(Retriever)
- 记忆/状态抽象(Memory/State)
- 输出解析抽象(Output Parser / Structured Output)
这样在你换模型、换向量库、换工具时,上层业务逻辑尽量不改。
LangChain 的流程化部分构造了一个 Runnable 体系(LangChain Expression Language LCEL):
- 每个节点都是“输入 -> 输出”的可运行单元
- 支持 pipe(顺序)、parallel(并行)、map、branch(分支)等组合
- 前一个节点输出可直接喂给下一个节点
这本质上是把 LLM 应用表达成一个“可执行的数据流图”。
LangGraph 关注编排层。
在 LangGraph 里,“编排”可以定义为:
把多个节点(Node)按图结构(Graph)组织起来,由运行时(Runtime)基于状态(State)动态调度执行,并通过检查点(Checkpoint)与中断(Interrupt)保证流程可持续运行。
在 LangGraph 语境里:
- 流程 = 你画出来的业务图(节点与边)
- 编排 = 让这张图“按状态正确跑起来”的整套机制
LLM抽象
LLM 层可以理解为“模型适配层”。它不是简单封装一个 HTTP 请求,而是把不同模型的输入、输出、调用方式和附加能力整理成一套稳定接口。
以 LangChain 为例,结构如下:
业务代码 / Chain / Agent
|
Prompt / Messages
|
ChatModel / LLM / Embeddings
|
Provider Adapter
OpenAI / Gemini / Ollama ...
|
具体模型 API
LangChain 在 LLM 层主要做几件事。
第一,统一消息结构。ChatModel 接收的不是某个厂商专有格式,而是一组标准消息。文档里常见的消息如下:
[
{ role: "system", content: "..." },
{ role: "user", content: "..." },
{ role: "assistant", content: "..." }
]
消息里最核心的是 role 和 content。role 用来区分 system、user、assistant、tool;content 可以是文本,也可以是多模态内容块。这样,多轮对话、工具结果、模型回复就能放进同一个上下文结构里。模型返回的 AIMessage 还会带上更多信息,具体在 ChatModel 小节展开。
第二,统一调用 API。LangChain 文档里 ChatModel 的主要调用方式是:
- invoke:一次输入,一次完整输出。
- stream:流式返回 AIMessageChunk,适合边生成边展示。
- batch:批量调用多个输入。
- bindTools:把工具定义绑定到模型,让模型可以返回 tool_calls。
- withStructuredOutput:要求模型按 schema 返回结构化结果。
所以应用代码可以写成:
const model = await initChatModel("openai:gpt-4.1", { temperature: 0.25 });
const result = await model.invoke("what's your name");
第三,统一模型能力。现代 Agent 不只是“问一句答一句”,还会用到工具调用、结构化输出、多模态输入、流式输出、token 统计、超时、重试、缓存、trace 等能力。LangChain 把这些能力挂在模型抽象和 Runnable 体系上,让模型可以接到 Prompt、Output Parser、Retriever、Tool、Agent 流程里。
所以,LLM 应用框架里的模型抽象,本质上是在定义一条边界:业务层负责“我要完成什么任务”,模型层负责“把这个任务翻译给具体模型,并把结果翻译回标准结构”。有了这层边界,换模型、加工具、改输出格式,才不会直接冲击整条业务流程。
ChatModel / LLM / Embeddings
这里要把三个词拆开看。它们都属于“模型抽象层”,但抽象的不是同一种能力:
- ChatModel:消息进,消息出。适合现代对话、工具调用、结构化输出、多模态。
- LLM:文本 prompt 进,文本 completion 出。更接近早期 completion API。
- Embeddings:文本进,向量出。服务检索、相似度搜索、RAG,不负责生成回答。
1. ChatModel:现代 Agent 最常用的模型接口
从 LangChain JS 的抽象设计看,ChatModel 是建立在通用语言模型基类之上的消息模型接口。它的 invoke() 可以概括为:把上层输入整理成统一消息格式,交给底层模型生成,再把结果包装回标准消息对象。
async invoke(input, options) {
const promptValue = BaseChatModel._convertInputToPromptValue(input);
const result = await this.generatePrompt([promptValue], options, options?.callbacks);
const chatGeneration = result.generations[0][0];
return chatGeneration.message;
}
这说明 ChatModel 对上层暴露的不是某个厂商的 HTTP 细节,而是一个稳定的“消息模型”接口。它还定义了 bindTools(),用来把工具描述绑定到模型上:
bindTools?(tools, kwargs): Runnable<...>;
这就是为什么 Agent 层通常依赖 ChatModel,而不是直接依赖某个 SDK。Agent 需要的不只是文本,还需要模型返回的 tool_calls、用量信息、元数据、流式消息片段等信息。LangChain 也会把这些字段放进 AIMessage 这样的统一消息结构里,供上层流程继续消费。
对应到 provider API,可以先看 OpenAI 这个最常见的例子:
// OpenAI Responses API
const response = await openai.responses.create({
model: "gpt-5.4",
input: [
{ role: "user", content: "Explain LangChain ChatModel." }
],
});
OpenAI 官方文档把 Responses API 描述成可以接收字符串或消息列表、支持工具和结构化输出的统一接口;Chat Completions 则是保留的 chat 形态。对新项目,OpenAI 当前更推荐优先考虑 Responses API。与此同时,官方文档也明确说明:从 GPT-5.4 开始,如果在 Chat Completions 中设置 reasoning: none,则不支持 tool calling。LangChain 的 ChatOpenAI 适配器会在这两种 OpenAI API 之间做选择,最后都翻译回统一的消息对象。
不同 provider 的字段、响应对象和工具调用细节并不完全一样。更一般地说,LangChain 的 provider adapter 会把这些差异压到下层;上层仍然写:
const result = await model.invoke(messages);
如果下沉到底层 HTTP API,看到的就不是 SDK 方法名,而是具体 endpoint、headers 和 JSON body。以 OpenAI 为例:
# OpenAI Chat Completions API
curl https://api.openai.com/v1/chat/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-d '{
"model": "gpt-5.4",
"messages": [
{"role": "developer", "content": "You are a helpful assistant."},
{"role": "user", "content": "Explain LangChain ChatModel."}
]
}'
OpenAI 还有更新的 Responses API(/v1/responses),支持字符串或消息列表输入,LangChain 会根据配置决定走哪条 OpenAI 调用路径。这里把 Chat Completions 和 Responses 都列出来,主要是为了说明底层 HTTP 形态,而不是说它们在新项目里同等推荐。前面提到的 OpenAI-compatible 模型在 HTTP 层面也是同理,只是 baseURL 不同;而其他 provider 的差异则由各自的 adapter 负责抹平。
2. LLM:传统 completion 模型接口
LangChain 里 LLM 和 ChatModel 不是一个东西。LLM 面向的是纯文本 completion 接口,它的 invoke() 可以概括为“输入一段 prompt,返回一段纯文本”:
async invoke(input, options): Promise<string> {
const promptValue = BaseLLM._convertInputToPromptValue(input);
const result = await this.generatePrompt([promptValue], options, options?.callbacks);
return result.generations[0][0].text;
}
也就是说,LLM 抽象关心的是:
prompt string -> completion string
而不是:
messages -> AIMessage
OpenAI 的 completion API 就是这个形态:
const completion = await openai.completions.create({
model: "gpt-3.5-turbo-instruct",
prompt: "Say this is a test.",
});
对应到底层 HTTP,就是 prompt -> text completion:
# OpenAI Completions API
curl https://api.openai.com/v1/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-d '{
"model": "gpt-3.5-turbo-instruct",
"prompt": "Explain LangChain LLM.",
"max_tokens": 128,
"temperature": 0
}'
LangChain 的 OpenAI LLM 适配器也对应这个接口,核心输入字段就是 prompt。同时它会把 completion model 和 chat model 区分开:如果你把典型 chat model 当成传统 LLM 来用,框架会提示改用 ChatOpenAI。
所以在现代 Agent 里,LLM 更像历史遗留或简单文本生成接口。只要你需要多轮消息、工具调用、结构化输出、多模态,就应该用 ChatModel。
3. Embeddings:向量模型接口
Embeddings 和前两者差异更大。它不是生成模型,不返回自然语言,而是把文本变成向量。LangChain core 里的抽象只有两个核心方法:
abstract embedDocuments(documents: string[]): Promise<number[][]>;
abstract embedQuery(document: string): Promise<number[]>;
也就是说:
多篇文档 -> 多个向量
一个查询 -> 一个向量
OpenAI Embeddings API 对应的是:
const embedding = await openai.embeddings.create({
model: "text-embedding-3-large",
input: "LangChain model abstraction",
});
HTTP API 形态是:
# OpenAI Embeddings API
curl https://api.openai.com/v1/embeddings \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"input": "LangChain model abstraction",
"model": "text-embedding-3-small",
"encoding_format": "float"
}'
不是所有 provider 都提供自己的 embedding model。工程上也常见生成模型和向量模型分属不同 provider;这里只先用一个例子说明 Embeddings 接口本身。
在 LangChain 的使用方式里,embedDocuments() 对应批量文档嵌入,通常会按 batch 组织请求,例如:
{
model: this.model,
input: batch,
dimensions: this.dimensions,
encoding_format: this.encodingFormat,
}
最后调用:
this.client.embeddings.create(request)
embedQuery() 则是同一套逻辑,只是输入是一条 query,返回一个向量。
这层抽象在 RAG 里很关键:
用户问题
-> embedQuery()
-> 向量库相似度搜索
-> 取回相关文档
-> 塞进 Prompt / Messages
-> ChatModel 生成回答
注意,Embeddings 的 provider 不一定和 ChatModel 的 provider 一样。工程上很常见的组合是:聊天模型负责生成回答,OpenAIEmbeddings 这样的 embedding 适配器负责检索向量。LangChain 把它们放在同一层,是因为它们都是”模型能力”,但职责完全不同。
所以这三个抽象的边界可以总结成:
ChatModel : Messages / PromptValue -> AIMessage
LLM : Prompt string -> string
Embeddings : text -> vector
对应到具体 API,各家 provider 暴露的是不同 endpoint、字段和响应结构;对应到 LangChain,上层统一调用 invoke()、stream()、bindTools()、embedQuery()、embedDocuments()。这就是模型抽象层存在的价值。
抽样策略
Sampling 是模型生成阶段的“抽样策略”。它不是 prompt 本身的能力,而是模型推理服务在“下一个 token 怎么选”这一层暴露出来的参数;在 API 或 LangChain 里,通常表现为 temperature、top_p 这类生成参数。不同 provider 的字段名、默认值和可调范围会有差异,所以工程上更适合把它理解成“多数生成模型都会提供的生成控制参数”。
在 HTTP API 里,它通常就是请求体里的生成参数。比如 OpenAI Responses API 里可以这样传:
curl https://api.openai.com/v1/responses \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-d '{
"model": "gpt-5.4",
"input": "用一句话解释 ReAct Agent。",
"temperature": 0.2,
"top_p": 1
}'
在 LangChain 里写:
const model = await initChatModel("openai:gpt-4.1", {
temperature: 0.2,
topP: 1,
});
这两段代码表达的是同一件事:把采样参数传给模型服务。LangChain 只是把上层统一参数转成具体 provider 的 HTTP 请求字段。
LLM 不是一次性吐出整段文字,而是逐 token 生成。每一步大致是:
上下文 tokens
|
Transformer 前向计算
|
得到词表中每个候选 token 的 logits
|
把 logits 转成概率分布
|
按解码策略选出下一个 token
|
追加到上下文,继续下一轮
这里的 logits 可以理解成模型给候选 token 打的原始分数。比如下一步可能是:
"Agent": 5.2
"模型" : 4.7
"系统" : 4.1
"苹果" : 0.3
这些分数会被转换成概率分布,然后再按解码策略选出下一个 token。最简单的策略是 greedy decoding,也就是永远选概率最高的 token,稳定但容易僵硬。
Sampling 的设计目的,就是在“总是选最高概率”之外,给生成过程一个可控的随机性。它要解决的不是“让模型更聪明”,而是控制输出的探索范围:
- 需要稳定、可复现、格式严格时,让模型更保守。
- 需要头脑风暴、写作、候选方案时,允许模型探索更多合理表达。
- 避免从概率极低的长尾 token 中乱选,减少跑偏和胡言乱语。
- 在质量、创造性、延迟、可调试性之间给应用层一个工程旋钮。
temperature 控制的是概率分布的“尖锐程度”。实现上通常是在 softmax 之前把 logits 除以 temperature:
probabilities = softmax(logits / temperature)
temperature 越低,原本高分 token 的优势会被放大,模型更倾向于选择最可能的 token;temperature 越高,分布会被拉平,低概率 token 也更容易被抽到,输出就更发散。
可以粗略理解成:
temperature = 0 接近每次都选最高概率 token,最保守
temperature = 0.2 更稳定,适合事实问答、结构化输出、工具参数
temperature = 0.7 平衡稳定和变化,适合一般对话和解释
temperature = 1.0+ 更随机,适合创意写作、头脑风暴
这里的 temperature = 0 在很多 API 里更接近 greedy decoding,但不等于绝对可复现;如果要强约束格式,仍然要配合 structured output、JSON schema、validator、重试和测试集。
top_p 也叫 nucleus sampling。它控制的不是分布尖锐程度,而是“候选 token 池有多大”。做法是先把 token 按概率从高到低排序,然后只保留累计概率达到 top_p 的最小 token 集合,再在这个集合里重新归一化并采样。
例如某一步的 token 概率是:
A: 0.50
B: 0.25
C: 0.12
D: 0.08
E: 0.05
如果 top_p = 0.8,候选集合会先包含 A、B、C,因为 0.50 + 0.25 + 0.12 = 0.87,已经超过 0.8。D 和 E 会被排除。这样模型不是只看固定数量的候选,而是根据当前分布动态决定候选池大小:
- 当模型很确定时,少数 token 就能覆盖大部分概率,候选池会很小。
- 当模型不确定时,需要更多 token 才能达到累计概率,候选池会变大。
这就是 nucleus sampling 的设计重点:既保留一定多样性,又切掉概率很低的长尾 token。相比只调 temperature,top_p 更像是在控制“允许模型从多长的候选尾巴里抽样”。
实际使用时通常不要同时大幅调整 temperature 和 top_p。二者都会影响随机性:temperature 改变整个概率分布的形状,top_p 截断候选集合。
实践上可以把它理解成一个简单原则:事实问答、RAG、工具参数、JSON 输出倾向于使用更低的 temperature;普通对话和改写保持中间值;创意写作、头脑风暴才适合提高随机性。具体数值仍然要用真实样例评测,而不是凭单次输出体感调参。
从 Agent 工程角度看,Sampling 只是模型层的一个辅助参数,但会直接影响 planning、tool calling、structured output 和 RAG answer 的稳定性。temperature 太高,Agent 更容易调用错工具、写错 JSON、偏离任务;temperature 太低,开放式探索时又可能缺少候选思路。核心原则是:根据任务风险和输出形态,在“确定性”和“多样性”之间选一个合适的位置。
Structured Output
LLM 应用里,模型的原始输出通常是文本或消息。但业务系统真正需要的往往不是一段自然语言,而是一个可以被程序继续处理的数据结构:
用户问题 -> LLM -> 文本
对聊天来说,这已经够了。但如果下一步要做路由、填表、调用 API、写数据库、生成 UI、进入审批流,只拿一段文本就不够稳定。应用更需要的是:
用户问题 -> LLM -> { category, confidence, reason }
或者:
用户问题 -> LLM -> {
"answer": "...",
"sources": [...],
"needs_followup": false
}
Output Parser 和 Structured Output 解决的就是这个问题:把模型输出从“给人读的文本”变成“给程序用的数据”。
这两个概念要分开看:
- Output Parser 是框架层抽象。它拿到模型输出,然后解析、校验、转换成应用需要的类型。
- Structured Output 是模型/API 层能力。调用模型时就告诉 provider:这次输出必须符合某个结构,通常是 JSON 或 JSON Schema。
它们经常一起出现,但不是一回事。Output Parser 可以解析普通文本,也可以解析 JSON;Structured Output 可以减少解析失败,但应用层通常仍然需要校验、错误处理和类型约束。
常见接口形态
JSON mode、Structured Output、Tool Calling 不是一个统一的跨厂商标准,也不是一个严格的单层分类,而是当前模型 API 和 LLM 应用框架中常见的三种结构化交互接口。下面以 OpenAI 文档为主例说明;其他 provider 往往只覆盖其中一部分,字段名和能力边界也可能不同。
所以这里更适合把它们理解成工程上的常见接口形态,而不是同一个协议里的标准 taxonomy。
第一种是 JSON mode / JSON Output。它要求模型输出合法 JSON,但不保证字段一定符合某个业务 schema。以 OpenAI Responses API 为例,核心格式是:
{
"text": {
"format": {
"type": "json_object"
}
}
}
OpenAI 文档明确要求:开启 JSON mode 时,除了设置 json_object,还要在 prompt 里显式要求输出 JSON;否则模型可能持续输出空白直到达到 token 上限。这里的关键点是:json_object 约束的是“输出是合法 JSON”,不是“输出一定符合你的业务 schema”。
第二种是 Structured Output / JSON Schema response format。它把目标结构作为 schema 传给模型服务。OpenAI 的 Structured Outputs 就是这个方向:使用 json_schema response format 或 Responses API 的 text.format,让模型输出符合给定 JSON Schema。
OpenAI 文档里有一个典型例子:让模型解数学题时,不只返回一段解释,而是按 schema 返回一组步骤字段和最终答案字段。这里的重点不是 JSON 语法本身,而是 API 请求里带了 schema,模型输出需要符合这个 schema。
OpenAI 文档把 Structured Outputs 和 JSON mode 的差异说得很清楚:两者都能让输出成为 valid JSON,但只有 Structured Outputs 的目标是让输出 adhere to schema。也就是说:
JSON mode:
保证是 JSON,但不保证字段结构完全正确。
Structured Output:
以 JSON Schema 描述目标结构,并要求模型按 schema 输出。
第三种是 Tool Calling / Function Calling。这时模型输出的结构不是最终答案,而是一次工具调用的参数。OpenAI 把 function/tool 的 parameters 定义成 JSON Schema 风格的结构。模型返回工具名和参数,应用代码再执行对应函数。
这里的 schema 约束的是工具输入参数。模型返回工具名和 arguments,应用代码再执行函数,并把工具结果交还给模型。所以 Tool Calling 也是 structured output 的一种,但它的目标不是“直接给用户一个结构化答案”,而是“让模型以结构化参数请求外部能力”。这就是为什么 OpenAI 文档建议:如果你是在连接模型和外部工具、函数、数据库、UI 动作,就用 function calling;如果你只是希望模型的最终回复符合某个结构,就用 structured response format。
为什么不能只靠 prompt
一种朴素写法是:
请严格返回 JSON,格式如下:
{
"category": "...",
"confidence": 0.0
}
这在 demo 里常常能工作,但生产系统不能只靠这种方式。原因是:
- 模型可能多输出解释文字,导致
JSON.parse()失败。 - 字段可能缺失,比如没有
confidence。 - 类型可能错误,比如把 number 输出成 string。
- enum 可能越界,比如输出了 schema 里没有的分类。
- JSON 可能因为
max_tokens被截断。 - 安全拒答时,模型可能返回 refusal,而不是目标 JSON。
- RAG 或用户输入里可能包含 prompt injection,诱导模型改变输出格式。
Structured Output 的价值,就是把“请你按格式输出”从自然语言请求,升级成 API 级约束。Output Parser 的价值,是在应用边界把这个结果再解析、校验、转换,并在失败时给出明确错误。
Structured Output 和 Agent 的关系
Agent 不只需要最终答案,很多中间决策也需要结构化,比如 routing、planning、evaluator:
{
"route": "refund",
"confidence": 0.92
}
{
"passed": false,
"reason": "答案没有引用来源",
"needs_retry": true
}
这些字段会影响 Agent Runtime 的下一步控制流:走哪个 node、调用哪个 tool、是否重试、是否转人工、是否结束。如果输出结构不稳定,Agent 的控制流就不稳定。
Output Parser 和 Structured Output 可以用一句话区分:
Structured Output 约束模型怎么输出。
Output Parser 负责把输出变成应用里的数据。
在 LLM 应用框架里,这一层非常关键。Prompt 让模型理解任务,Model 负责生成,Output Parser / Structured Output 负责把生成结果变成可靠的程序接口。没有这一层,LLM 应用很容易停留在“能聊天”;有了这一层,LLM 才能稳定进入路由、工具调用、RAG 引用、审批流和业务系统。
Tool
Tool 可以理解成“模型可以请求调用的外部能力”。它让 LLM 不只是在上下文里生成文本,还能去查数据、调用 API、执行代码、读写状态,再把结果交还给模型继续推理。
在 LangChain 里,Tool 大概处在这个位置:
用户问题
|
ChatModel
|
AIMessage.tool_calls
|
ToolNode / Agent Runtime
|
具体 Tool.invoke(...)
|
ToolMessage
|
再回到 ChatModel
也就是说,模型本身通常不直接执行函数。模型负责判断“要不要调用工具、调用哪个工具、参数是什么”;LangChain 负责把这个 tool call 找到对应工具、校验参数、执行函数、把结果包装成 ToolMessage,再放回消息列表。
LangChain 里的一个 Tool 至少包含几类信息:
- name:工具名,模型会用这个名字发起调用。文档建议用 snake_case,避免空格和特殊字符。
- description:告诉模型这个工具什么时候该用。
- schema:工具输入参数的结构,通常用 Zod 或 JSON Schema 定义。
- invoke:真正执行工具调用的统一入口。
一个典型工具定义是这样:
const search = tool(
({ query }) => `Results for: ${query}`,
{
name: "search",
description: "Search for information",
schema: z.object({
query: z.string().describe("The search query"),
}),
}
);
这里的重点不是函数本身,而是 schema 和 description。schema 约束模型生成的参数,description 帮模型判断什么时候使用工具。LangChain 的工具抽象也基本围绕 name / description / schema / invoke 展开:调用时会先按 schema 解析参数,再执行工具逻辑,最后把结果格式化为工具输出。
工具调用在消息里通常表现为两段。
第一段是模型返回的 AIMessage,里面带 tool_calls:
{
role: "assistant",
content: "",
tool_calls: [
{
id: "call_123",
name: "search",
args: { query: "ReAct agents" },
type: "tool_call"
}
]
}
第二段是工具执行后的 ToolMessage:
{
role: "tool",
tool_call_id: "call_123",
content: "Results for: ReAct agents"
}
tool_call_id 很关键,它把某一次模型发出的 tool call 和工具返回结果对应起来。尤其是并行调用多个工具时,Agent 需要靠这个 id 把结果放回正确的位置。
如果只是把工具绑定到模型,可以使用 bindTools:
const modelWithTools = model.bindTools([search]);
const aiMessage = await modelWithTools.invoke("Search for ReAct agents");
这一步只是让模型“知道有哪些工具可以调用”,并让模型有机会返回 tool_calls。如果要完整跑完“模型决定调用工具 -> 执行工具 -> 把结果给模型 -> 继续回答”这个循环,通常交给 Agent 或 ToolNode:
const agent = createAgent({
model: "openai:gpt-4.1",
tools: [search],
});
const result = await agent.invoke({
messages: [{ role: "user", content: "Search for ReAct agents" }],
});
在 LangChain 的 Agent 文档里,createAgent 会构建一个基于 LangGraph 的运行时。图里有 model node,也有 tools node。model node 负责调用模型;tools node 负责执行工具;如果模型继续返回 tool_calls,就继续进入工具节点,否则结束。
Tool 层还承担了一些工程化能力:
- 参数校验:schema 不匹配时,运行时会返回明确的参数解析错误。
- 错误处理:ToolNode 可以配置工具错误如何返回给模型。
- 并行执行:多个 tool_calls 可以由 ToolNode 执行。
- 状态访问:工具可以通过运行时注入的信息访问上下文、状态存储和执行信息。
- 返回值规范:工具可以返回字符串、对象,或者在 LangGraph 场景中返回
Command来更新状态。
所以 Tool 抽象的核心不是“把函数传给模型”,而是给外部能力建立一个稳定协议:用 name 和 description 暴露能力,用 schema 约束输入,用 ToolMessage 回传结果,用 Agent/ToolNode 把工具调用接进模型循环。这样,LLM 就能在受控边界内调用外部能力。
Runnable / Pipe
Runnable 是 LangChain 里非常核心的一个抽象。可以把它理解成一个统一的“可执行节点”:
输入
|
Runnable.invoke(input)
|
输出
Prompt、Model、Output Parser、Retriever、Tool 都可以是 Runnable。它们内部做的事情不同,但对外都暴露一套相似的调用方式。
从 LangChain 的接口设计看,Runnable 最基础的 API 是:
- invoke(input):单次调用。
- batch(inputs):批量调用。默认实现是对每个 input 调 invoke,并支持并发配置。
- stream(input):流式输出。默认实现会退化成一次 invoke;模型等组件可以覆盖自己的流式逻辑。
- pipe(next):把当前 Runnable 的输出接给下一个 Runnable。
- withConfig(config):给调用绑定 tags、metadata、callbacks 等配置。
- withRetry() / withFallbacks():给 Runnable 加重试或兜底逻辑。
最典型的 pipe case 是 Prompt -> Model -> Parser:
const chain = prompt.pipe(model).pipe(parser);
const result = await chain.invoke({
topic: "LangChain",
});
它表达的是:
{ topic: "LangChain" }
|
PromptTemplate
|
PromptValue / Messages
|
ChatModel
|
AIMessage
|
OutputParser
|
string / object
这里每一步的输出,都会成为下一步的输入。实现上,pipe() 会把多个步骤串成一个顺序执行的序列:上一步的结果直接成为下一步的输入。
所以 Runnable 不是为了让代码写成链式形式,而是为了给不同组件建立统一执行协议。统一以后,框架才能在同一条链上做 batch、stream、callback、trace、retry、fallback。
除了顺序 pipe,Runnable 还可以表达并行的数据准备。比如 RAG 里常见的结构:
用户问题
|
+-> retriever -> context
|
+-> passthrough -> question
|
v
prompt
|
model
|
parser
对应的数据形态大概是:
{
context: retriever,
question: new RunnablePassthrough(),
}
这表示同一个输入会同时喂给 retriever 和 passthrough,最后组合成 { context, question },再交给 prompt。
从这个角度看,Runnable / pipe 解决的是 LLM 应用里的“流程组合”问题:把原本散落的 prompt 调用、模型调用、解析、检索、工具等步骤,变成一组可以串联、并联、追踪、复用的执行单元。
以上就是本文的主要内容。
参考资料
- Shunyu Yao et al., ReAct: Synergizing Reasoning and Acting in Language Models, ICLR 2023
- LangChain Docs, Agents
- Ehud Karpas et al., MRKL Systems
- Patrick Lewis et al., Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks
- OpenAI Agents SDK Docs, Agent Orchestration
- OpenAI Docs: Responses, Embeddings, Structured model outputs, Function calling