LLM Agent 浅析

过去几年里,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 轨迹如下:

text 复制代码
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]

这里的重点不是这些字段名,而是循环结构:

text 复制代码
推理 -> 行动 -> 观察 -> 再推理 -> 再行动 -> ...

论文里把这个关系说得很清楚:reasoning traces 帮模型创建、维护和调整行动计划,也能处理异常;actions 则让模型接入外部知识源或环境,把新的信息带回推理过程。换句话说,Thought 负责"决定下一步为什么这样做",Action 负责"去外部世界拿证据或产生影响",Observation 负责"把世界的反馈写回上下文"。

这也是为什么 ReAct 对 Agent 框架影响很大。一个 Agent 不再只是:

text 复制代码
用户输入 -> LLM -> 最终回答

而变成:

text 复制代码
用户输入
  |
LLM 生成下一步决策
  |
如果需要工具:执行 Tool
  |
把 Tool 结果作为 Observation 放回上下文
  |
再次调用 LLM
  |
直到输出最终答案

LangChain 的 Agent 文档里就能看到这个范式的工程化版本。LangChain 的核心 Agent 实现直接沿用了 ReAct 的命名和结构,内部构建了一个"模型决策 → 工具执行 → 结果反馈"的循环:模型判断是否需要调用工具以及用什么参数,runtime 执行工具并把结果放回上下文,模型再次决策,直到给出最终答案或达到循环上限。

在具体消息结构里,这个循环大概对应成:

text 复制代码
HumanMessage
  -> AIMessage(tool_calls=[...])
  -> ToolMessage(content="工具返回结果")
  -> AIMessage(tool_calls=[...] 或 final answer)

也就是说,早期 ReAct prompt 里显式写出来的 Thought / Action / Observation,在现代工具调用模型和 Agent runtime 里不一定都以文本字段出现。Action 变成结构化的 tool_callsObservation 变成 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 逐渐形成了几类常见构建方式。

先看一张关系图:

text 复制代码
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 可以理解成:

text 复制代码
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,影响下一次尝试。常见结构是:

text 复制代码
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 toolsHandoffs;前者是 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 角色明显、上下文可隔离、子任务可并行的复杂任务

实际落地时,通常不是只选一种,而是把几种模式组合起来:

text 复制代码
企业知识问答:
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 为例,结构如下:

text 复制代码
业务代码 / Chain / Agent
        |
Prompt / Messages
        |
ChatModel / LLM / Embeddings
        |
Provider Adapter
OpenAI / Gemini / Ollama ...
        |
具体模型 API

LangChain 在 LLM 层主要做几件事。

第一,统一消息结构。ChatModel 接收的不是某个厂商专有格式,而是一组标准消息。文档里常见的消息如下:

ts 复制代码
[
  { 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 返回结构化结果。

所以应用代码可以写成:

ts 复制代码
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() 可以概括为:把上层输入整理成统一消息格式,交给底层模型生成,再把结果包装回标准消息对象。

ts 复制代码
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(),用来把工具描述绑定到模型上:

ts 复制代码
bindTools?(tools, kwargs): Runnable<...>;

这就是为什么 Agent 层通常依赖 ChatModel,而不是直接依赖某个 SDK。Agent 需要的不只是文本,还需要模型返回的 tool_calls、用量信息、元数据、流式消息片段等信息。LangChain 也会把这些字段放进 AIMessage 这样的统一消息结构里,供上层流程继续消费。

对应到 provider API,可以先看 OpenAI 这个最常见的例子:

ts 复制代码
// 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 会把这些差异压到下层;上层仍然写:

ts 复制代码
const result = await model.invoke(messages);

如果下沉到底层 HTTP API,看到的就不是 SDK 方法名,而是具体 endpoint、headers 和 JSON body。以 OpenAI 为例:

bash 复制代码
# 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 里 LLMChatModel 不是一个东西。LLM 面向的是纯文本 completion 接口,它的 invoke() 可以概括为"输入一段 prompt,返回一段纯文本":

ts 复制代码
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 抽象关心的是:

text 复制代码
prompt string -> completion string

而不是:

text 复制代码
messages -> AIMessage

OpenAI 的 completion API 就是这个形态:

ts 复制代码
const completion = await openai.completions.create({
  model: "gpt-3.5-turbo-instruct",
  prompt: "Say this is a test.",
});

对应到底层 HTTP,就是 prompt -> text completion

bash 复制代码
# 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 里的抽象只有两个核心方法:

ts 复制代码
abstract embedDocuments(documents: string[]): Promise<number[][]>;
abstract embedQuery(document: string): Promise<number[]>;

也就是说:

text 复制代码
多篇文档 -> 多个向量
一个查询 -> 一个向量

OpenAI Embeddings API 对应的是:

ts 复制代码
const embedding = await openai.embeddings.create({
  model: "text-embedding-3-large",
  input: "LangChain model abstraction",
});

HTTP API 形态是:

bash 复制代码
# 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 组织请求,例如:

ts 复制代码
{
  model: this.model,
  input: batch,
  dimensions: this.dimensions,
  encoding_format: this.encodingFormat,
}

最后调用:

ts 复制代码
this.client.embeddings.create(request)

embedQuery() 则是同一套逻辑,只是输入是一条 query,返回一个向量。

这层抽象在 RAG 里很关键:

text 复制代码
用户问题
  -> embedQuery()
  -> 向量库相似度搜索
  -> 取回相关文档
  -> 塞进 Prompt / Messages
  -> ChatModel 生成回答

注意,Embeddings 的 provider 不一定和 ChatModel 的 provider 一样。工程上很常见的组合是:聊天模型负责生成回答,OpenAIEmbeddings 这样的 embedding 适配器负责检索向量。LangChain 把它们放在同一层,是因为它们都是"模型能力",但职责完全不同。

所以这三个抽象的边界可以总结成:

text 复制代码
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 里,通常表现为 temperaturetop_p 这类生成参数。不同 provider 的字段名、默认值和可调范围会有差异,所以工程上更适合把它理解成"多数生成模型都会提供的生成控制参数"。

在 HTTP API 里,它通常就是请求体里的生成参数。比如 OpenAI Responses API 里可以这样传:

bash 复制代码
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 里写:

ts 复制代码
const model = await initChatModel("openai:gpt-4.1", {
  temperature: 0.2,
  topP: 1,
});

这两段代码表达的是同一件事:把采样参数传给模型服务。LangChain 只是把上层统一参数转成具体 provider 的 HTTP 请求字段。

LLM 不是一次性吐出整段文字,而是逐 token 生成。每一步大致是:

text 复制代码
上下文 tokens
  |
Transformer 前向计算
  |
得到词表中每个候选 token 的 logits
  |
把 logits 转成概率分布
  |
按解码策略选出下一个 token
  |
追加到上下文,继续下一轮

这里的 logits 可以理解成模型给候选 token 打的原始分数。比如下一步可能是:

text 复制代码
"Agent": 5.2
"模型" : 4.7
"系统" : 4.1
"苹果" : 0.3

这些分数会被转换成概率分布,然后再按解码策略选出下一个 token。最简单的策略是 greedy decoding,也就是永远选概率最高的 token,稳定但容易僵硬。

Sampling 的设计目的,就是在"总是选最高概率"之外,给生成过程一个可控的随机性。它要解决的不是"让模型更聪明",而是控制输出的探索范围:

  • 需要稳定、可复现、格式严格时,让模型更保守。
  • 需要头脑风暴、写作、候选方案时,允许模型探索更多合理表达。
  • 避免从概率极低的长尾 token 中乱选,减少跑偏和胡言乱语。
  • 在质量、创造性、延迟、可调试性之间给应用层一个工程旋钮。

temperature 控制的是概率分布的"尖锐程度"。实现上通常是在 softmax 之前把 logits 除以 temperature:

text 复制代码
probabilities = softmax(logits / temperature)

temperature 越低,原本高分 token 的优势会被放大,模型更倾向于选择最可能的 token;temperature 越高,分布会被拉平,低概率 token 也更容易被抽到,输出就更发散。

可以粗略理解成:

text 复制代码
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 概率是:

text 复制代码
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 更像是在控制"允许模型从多长的候选尾巴里抽样"。

实际使用时通常不要同时大幅调整 temperaturetop_p。二者都会影响随机性:temperature 改变整个概率分布的形状,top_p 截断候选集合。

实践上可以把它理解成一个简单原则:事实问答、RAG、工具参数、JSON 输出倾向于使用更低的 temperature;普通对话和改写保持中间值;创意写作、头脑风暴才适合提高随机性。具体数值仍然要用真实样例评测,而不是凭单次输出体感调参。

从 Agent 工程角度看,Sampling 只是模型层的一个辅助参数,但会直接影响 planning、tool calling、structured output 和 RAG answer 的稳定性。temperature 太高,Agent 更容易调用错工具、写错 JSON、偏离任务;temperature 太低,开放式探索时又可能缺少候选思路。核心原则是:根据任务风险和输出形态,在"确定性"和"多样性"之间选一个合适的位置。

Structured Output

LLM 应用里,模型的原始输出通常是文本或消息。但业务系统真正需要的往往不是一段自然语言,而是一个可以被程序继续处理的数据结构:

text 复制代码
用户问题 -> LLM -> 文本

对聊天来说,这已经够了。但如果下一步要做路由、填表、调用 API、写数据库、生成 UI、进入审批流,只拿一段文本就不够稳定。应用更需要的是:

text 复制代码
用户问题 -> LLM -> { category, confidence, reason }

或者:

text 复制代码
用户问题 -> 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 为例,核心格式是:

json 复制代码
{
  "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。也就是说:

text 复制代码
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

一种朴素写法是:

text 复制代码
请严格返回 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:

json 复制代码
{
  "route": "refund",
  "confidence": 0.92
}
json 复制代码
{
  "passed": false,
  "reason": "答案没有引用来源",
  "needs_retry": true
}

这些字段会影响 Agent Runtime 的下一步控制流:走哪个 node、调用哪个 tool、是否重试、是否转人工、是否结束。如果输出结构不稳定,Agent 的控制流就不稳定。

Output Parser 和 Structured Output 可以用一句话区分:

text 复制代码
Structured Output 约束模型怎么输出。
Output Parser 负责把输出变成应用里的数据。

在 LLM 应用框架里,这一层非常关键。Prompt 让模型理解任务,Model 负责生成,Output Parser / Structured Output 负责把生成结果变成可靠的程序接口。没有这一层,LLM 应用很容易停留在"能聊天";有了这一层,LLM 才能稳定进入路由、工具调用、RAG 引用、审批流和业务系统。

Tool

Tool 可以理解成"模型可以请求调用的外部能力"。它让 LLM 不只是在上下文里生成文本,还能去查数据、调用 API、执行代码、读写状态,再把结果交还给模型继续推理。

在 LangChain 里,Tool 大概处在这个位置:

text 复制代码
用户问题
  |
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:真正执行工具调用的统一入口。

一个典型工具定义是这样:

ts 复制代码
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:

ts 复制代码
{
  role: "assistant",
  content: "",
  tool_calls: [
    {
      id: "call_123",
      name: "search",
      args: { query: "ReAct agents" },
      type: "tool_call"
    }
  ]
}

第二段是工具执行后的 ToolMessage:

ts 复制代码
{
  role: "tool",
  tool_call_id: "call_123",
  content: "Results for: ReAct agents"
}

tool_call_id 很关键,它把某一次模型发出的 tool call 和工具返回结果对应起来。尤其是并行调用多个工具时,Agent 需要靠这个 id 把结果放回正确的位置。

如果只是把工具绑定到模型,可以使用 bindTools

ts 复制代码
const modelWithTools = model.bindTools([search]);
const aiMessage = await modelWithTools.invoke("Search for ReAct agents");

这一步只是让模型"知道有哪些工具可以调用",并让模型有机会返回 tool_calls。如果要完整跑完"模型决定调用工具 -> 执行工具 -> 把结果给模型 -> 继续回答"这个循环,通常交给 Agent 或 ToolNode:

ts 复制代码
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 里非常核心的一个抽象。可以把它理解成一个统一的"可执行节点":

text 复制代码
输入
 |
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:

ts 复制代码
const chain = prompt.pipe(model).pipe(parser);

const result = await chain.invoke({
  topic: "LangChain",
});

它表达的是:

text 复制代码
{ topic: "LangChain" }
        |
PromptTemplate
        |
PromptValue / Messages
        |
ChatModel
        |
AIMessage
        |
OutputParser
        |
string / object

这里每一步的输出,都会成为下一步的输入。实现上,pipe() 会把多个步骤串成一个顺序执行的序列:上一步的结果直接成为下一步的输入。

所以 Runnable 不是为了让代码写成链式形式,而是为了给不同组件建立统一执行协议。统一以后,框架才能在同一条链上做 batch、stream、callback、trace、retry、fallback。

除了顺序 pipe,Runnable 还可以表达并行的数据准备。比如 RAG 里常见的结构:

text 复制代码
用户问题
  |
  +-> retriever       -> context
  |
  +-> passthrough     -> question
        |
        v
      prompt
        |
      model
        |
      parser

对应的数据形态大概是:

ts 复制代码
{
  context: retriever,
  question: new RunnablePassthrough(),
}

这表示同一个输入会同时喂给 retriever 和 passthrough,最后组合成 { context, question },再交给 prompt。

从这个角度看,Runnable / pipe 解决的是 LLM 应用里的"流程组合"问题:把原本散落的 prompt 调用、模型调用、解析、检索、工具等步骤,变成一组可以串联、并联、追踪、复用的执行单元。

以上就是本文的主要内容。

参考资料

相关推荐
我叫黑大帅2 小时前
TypeScript 6.0 弃用选项错误 TS5101 解决方法
javascript·后端·面试
科雷软件测试2 小时前
使用python+Midscene.js AI驱动打造企业级WEB自动化解决方案
前端·javascript·python
冬奇Lab2 小时前
一天一个开源项目(第80篇):Browser Harness - 让 AI 智能体拥有“手”与“眼”的轻量化浏览器桥梁
人工智能·开源·资讯
ConardLi2 小时前
把 Claude Design 做成 Skill,你的网站也能拥有顶级视觉体验
前端·人工智能·后端
We་ct2 小时前
LeetCode 120. 三角形最小路径和:动态规划详解
前端·javascript·算法·leetcode·typescript·动态规划
ZhengEnCi3 小时前
01c-LSTM与GRU门控机制详解
人工智能
科技林总3 小时前
自然语言处理任务分类
人工智能·自然语言处理
谈思汽车3 小时前
当 AI 走进工厂与家庭:谁来保护AIoT 的“最后一米”?
人工智能·物联网·智能家居·健康医疗