Skip to content
概述
一次 API 调用只完成一次模型推理。要完成一个多步骤任务,需要让模型运行在一个循环中。这个循环通常包含四个阶段:Reasoning(推理)、Planning(规划)、Action(行动)和 Observation(观察)。
在循环中,模型根据当前状态决定做什么,系统执行模型选择的动作,执行结果作为新的输入返回给模型,模型继续推理,直到任务结束。LangChain 官方文档将 Agent 定义为“在循环中调用工具,直到任务完成的模型”(An agent is a model calling tools in a loop until a given task is complete)。循环之外的部分被称为 harness,负责提供 prompt、工具和 middleware,在正确的时间为模型提供正确的上下文。
以“北京明天的天气适合跑步吗”为例,模型不能直接给出可靠答案。它需要先查询北京明天的天气,再根据温度、降雨等条件判断,最后回答。执行查询和读取查询结果发生在不同的模型调用之间,因此需要循环。
基本概念
一个典型的 Agent 执行循环包含四个阶段:
| 阶段 | 回答的问题 |
|---|---|
| Reasoning | 当前状态是什么?目标是什么?下一步有哪些可能? |
| Planning | 从当前状态到目标,需要按什么顺序执行哪些步骤? |
| Action | 执行具体动作:调用工具、返回回答、请求更多信息。 |
| Observation | 动作产生了什么结果?结果如何更新当前状态? |
循环的推进方式:
当前状态 -> Reasoning -> Planning -> Action -> Observation -> 新的状态循环不断重复,直到模型不再调用工具,或者满足终止条件。
单次模型调用无法替代循环。模型本身无法查询外部数据库、调用计算器,也无法在返回结果后继续调整策略。只有把模型放入由外部环境驱动的循环中,它才能完成需要多步操作的任务。
工作原理
推理:当前状态分析
Reasoning 是模型对当前情况的内部思考。它回答“现在发生了什么”“我的目标是什么”“哪条路径更合理”。推理不直接对外输出,而是决定下一步选择。
以“帮我预订明天早上从北京到上海的机票”为例,模型的推理可能是:
- 用户需要机票;
- 需要知道具体出发时间,但用户没提供;
- 可以询问用户,或使用系统保存的历史偏好。
这个推理会引导后续的规划和行动。没有推理,模型可能直接调用订票工具,却不检查参数是否齐全。
链式思考(Chain-of-Thought)让模型在最终输出前生成中间推理步骤。这些步骤对用户可能是隐藏的,也可以被记录到日志中。在循环中,推理内容的可观察性帮助开发者判断模型是否抓住了问题核心。
下面这段代码展示一个假设的模型响应,其中包含推理字段:
javascript
const response = await callModel([
{ role: 'user', content: '北京明天适合跑步吗?' }
]);
// 假设 response.reasoning 是模型生成的中间思考
console.log(response.reasoning);
// 输出示例:
// 用户想知道明天是否适合跑步。这取决于天气。
// 我需要先获取北京的天气预报,再根据温度、降水、风力判断。在实际实现中,reasoning 可以来自模型返回的 reasoning_content 字段,也可以是 Prompt 中要求模型先思考再输出的文本。推理内容不等于最终答案。只有模型在 Action 阶段选择“直接回答”时,content 字段才是面向用户的输出。
规划:从目标到行动计划
规划生成行动计划。计划可以在推理之后一次性生成,也可以与推理交替进行。
- 先推理,再规划:模型先分析约束和资源,然后生成一个完整计划,后续执行阶段较少调用模型。适合目标明确、步骤稳定的任务。
- 边推理,边规划:每一步行动前做一次小规模推理,动态决定下一步。适合需要根据观察结果频繁调整的任务。
两种顺序不是互斥的。很多循环实现会先让模型生成一个初始计划,然后在执行过程中允许模型修改计划。
计划由结构化步骤组成。最简单的表示是数组,每个元素包含动作名称和参数:
javascript
const plan = [
{ action: 'search', args: { query: '北京 天气预报' } },
{ action: 'decide', args: { rule: '如果降水概率小于 30% 则适合跑步' } },
{ action: 'answer', args: {} },
];计划应保存在状态中,而不是只出现在模型输出的文本里。保存计划有三个作用:
- 执行器可以按步骤推进;
- 模型在后续推理中可以看到计划,便于调整;
- 日志中可以记录“计划与实际执行”的偏差。
当环境反馈与计划不一致时,Agent 需要重新规划。重新规划可能基于错误信息,也可能基于外部环境变化,例如用户改变了需求。
行动:工具调用与回答的统一出口
Action 是 Agent 对外部世界产生的影响。在循环中,Action 有三种主要形式:
| 类型 | 说明 | 示例 |
|---|---|---|
| 直接回答 | 将最终结果返回给用户 | “明天多云,温度 22°C,适合跑步。” |
| 调用工具 | 执行一次外部操作并获取结果 | 搜索、计算、读写数据库 |
| 请求信息 | 向用户询问缺少的参数 | “请提供出发时间。” |
在代码中,Action 通常表示成结构化对象,而不是自然语言文本,这样执行器才能可靠地分发:
javascript
{
type: 'call_tool',
name: 'get_weather',
arguments: { city: '北京' }
}Function Calling(或称 Tool Calling)是模型根据工具的元数据输出结构化调用参数的能力。工具元数据包含名称、描述和参数 schema。LangChain 中可以通过 tool 函数声明工具,参数用 zod 描述:
javascript
import { tool } from 'langchain';
import { z } from 'zod';
const search = tool(
({ query }) => `关于 ${query} 的搜索结果`,
{
name: 'search',
description: '搜索并返回摘要',
schema: z.object({ query: z.string() }),
}
);当模型决定调用工具时,它会返回类似下面的 JSON 对象:
json
{
"name": "search",
"arguments": {
"query": "LLM Agent"
}
}Agent 运行环境负责将 name 解析到具体函数,用 arguments 调用它,然后把返回值包装成一条消息。工具选择可以由模型决定,也可以通过规则限制:只把可信的工具暴露给模型,就是一种安全约束。
观察:反馈如何进入下一轮循环
Observation 是工具执行结果的回传。在 OpenAI 兼容的消息格式中,它通常表示为一条 role: 'tool' 消息:
javascript
{
role: 'tool',
toolCallId: 'call_123',
content: '北京:晴,20°C,降水概率 10%'
}这条消息会追加到对话上下文中,供模型下一轮推理使用。如果没有 Observation,模型无法知道上一轮动作是否成功,也无法据此调整下一步。
工具执行后没有把结果写入消息历史,模型下一轮看到的上下文与上一轮完全一样,它很可能重复相同的工具调用,造成无限循环。因此,每个 Action 都必须有对应的 Observation,且观察内容必须对模型可见。
终止条件
循环不是无限运行的。常见终止条件包括:
- 模型不再输出工具调用:表示模型给出了最终回答;
- 达到最大步数:防止因模型行为异常而无限循环;
- 用户取消:在循环开始前检查外部标志;
- 预算耗尽:通过 token 计数或成本计数强制停止。
在最小实现中,最大步数是最重要的保护机制。
基本用法
一个最小 Agent 循环的用法如下:
- 准备系统提示和用户任务组成的初始消息列表;
- 调用模型,获得响应;
- 将响应追加到消息列表;
- 若响应中没有工具调用,返回
content作为最终回答; - 若有工具调用,逐个执行;
- 将执行结果作为
tool消息追加到消息列表; - 回到第 2 步。
核心循环可表示为以下 ES6 骨架:
javascript
async function callModel(messages) {
// 接入具体模型 API
// 返回 { content: string | null, toolCalls: Array | null }
}
async function executeTool(call) {
// 根据 call.name 分发到具体工具
}
async function runAgent(task, { maxSteps = 5 } = {}) {
const messages = [
{ role: 'system', content: '你是一个能调用工具的助手。' },
{ role: 'user', content: task },
];
for (let step = 0; step < maxSteps; step++) {
const response = await callModel(messages);
messages.push({
role: 'assistant',
content: response.content,
toolCalls: response.toolCalls,
});
if (!response.toolCalls || response.toolCalls.length === 0) {
return response.content;
}
for (const call of response.toolCalls) {
try {
const result = await executeTool(call);
messages.push({
role: 'tool',
toolCallId: call.id,
content: JSON.stringify(result),
});
} catch (err) {
messages.push({
role: 'tool',
toolCallId: call.id,
content: `工具执行失败: ${err.message}`,
});
}
}
}
return '已达到最大步数,任务未完成。';
}runAgent 的循环结构是:调用模型、检查是否有工具调用、执行工具、把观察结果加入消息历史。没有工具调用时,模型给出的文本就是最终回答。
try/catch 分支把工具执行失败写成一条观察结果传回模型。模型可以基于错误信息更换工具或改变参数,而不是让整个循环崩溃。工具错误被视为普通输入,而不是终止条件。
API
以下接口是上述示例中的语义约定。实际项目中,它们由具体框架提供,例如 LangChain 的 createAgent 和 tool。
callModel(messages)
向模型发送消息列表,返回模型响应。
- 参数:
messages— 消息数组,元素包含role、content,工具调用记录可包含toolCalls。 - 返回:
{ content, toolCalls }。content是文本输出;toolCalls是结构化工具调用列表,没有工具调用时为null。
executeTool(call)
根据工具调用对象分发到具体工具函数。
- 参数:
call— 形如{ id, name, arguments }的对象。 - 返回:工具返回值的
Promise。 - 异常:工具不存在或执行失败时抛出错误。
runAgent(task, options)
运行一个最小 Agent 循环。
task:用户任务字符串。options.maxSteps:最大循环轮数,默认值为 5。- 返回:最终回答字符串。
registerTool(name, description, fn)
注册一个工具,使其可以通过名称被 executeTool 找到。
name:工具名。description:工具说明,供模型决定是否调用。fn:异步函数,接收arguments对象作为参数。
createAgent({ model, tools })
LangChain JavaScript 中创建最小 Agent 的接口。模型通过字符串形式指定,例如 'openai:gpt-5.5',tools 是工具数组。基础配置参数为 model、tools、system_prompt。更高级的能力通过 middleware 扩展,例如:
javascript
import { humanInTheLoopMiddleware } from 'langchain';
const agent = createAgent({
model: 'openai:gpt-5.5',
tools: [search],
system_prompt: '你是一个能调用工具的助手。',
middleware: [
humanInTheLoopMiddleware({
interruptOn: { writeFile: true },
}),
],
});humanInTheLoopMiddleware 会在模型调用 writeFile 工具时插入人工审批。
示例
下面演示一个可运行的 ES6 风格循环骨架。代码不绑定具体模型和框架,只展示核心结构。
状态结构
用普通对象表示状态:
javascript
const state = {
task: '查询北京天气并决定是否适合跑步',
messages: [],
plan: [],
step: 0,
maxSteps: 5,
};messages保存与模型对话的完整历史;plan保存当前计划;step记录循环次数;maxSteps控制最大循环次数。
工具注册与调用封装
工具注册表用普通对象实现:
javascript
const registry = {};
function registerTool(name, description, fn) {
registry[name] = { description, fn };
}
async function executeTool(call) {
const item = registry[call.name];
if (!item) {
throw new Error(`未知工具: ${call.name}`);
}
return item.fn(call.arguments);
}注册两个简单工具:
javascript
registerTool('search', '搜索并返回摘要', async ({ query }) => {
return `关于“${query}”的摘要`;
});
registerTool('calculator', '计算数学表达式', async ({ expression }) => {
// 实际项目中不要使用 eval
return Function(`'use strict'; return (${expression})`)();
});executeTool 解析模型返回的 toolCall,查找注册表,执行函数,并返回结果。任何错误都会被上层捕获,并作为观察结果传回模型。
一次完整的调用过程
输入:
javascript
const answer = await runAgent('查询北京天气并决定是否适合跑步');
console.log(answer);模型第一轮响应可能包含一个 search 工具调用。执行器查询注册表,调用搜索工具,把结果作为 tool 消息追加到历史,然后进入下一轮。模型看到搜索结果后,若不再输出工具调用,runAgent 返回其文本回答。
上下文裁剪
模型上下文窗口有限,消息不能无限增长。一种简单策略是只保留最近 N 条消息,同时保留系统消息:
javascript
function trimMessages(messages, keep = 8) {
const system = messages.filter((m) => m.role === 'system');
const rest = messages.filter((m) => m.role !== 'system');
return [...system, ...rest.slice(-keep)];
}trimMessages 在每次模型调用前使用。它的缺点是可能丢失早期工具结果。更完整的方式是用摘要模型把旧历史压缩成摘要,再放入上下文。
注意点
区分推理结果与最终输出
模型可能在推理中已经得出答案,但形式上仍没有结束循环。例如:
model response:
reasoning: "天气很好,适合跑步。"
content: null
toolCalls: null如果系统直接用 content 作为最终回答,用户会看到空结果。应区分两类输出:
- 推理字段:内部思考,不上屏;
- 内容字段:面对用户的最终回答。
循环只有在内容字段非空,且没有工具调用时,才把内容返回给用户。
每个 Action 都要有对应的 Observation
工具执行完成或失败后,必须把结果写入消息历史。缺少观察会让模型在下一轮看到与上一轮相同的上下文,从而可能重复相同的工具调用。
过规划与规划不足
过规划指模型生成了过于详细的步骤列表,但环境变化会导致计划很快失效。规划不足指模型没有生成计划,每一步都在重新决策,导致上下文混乱。合理做法是在计划中保留“根据观察结果修改计划”的出口,让计划具有弹性。
重复动作检测
即使有最大步数,模型也可能反复调用同一个工具。可以在状态中记录工具调用签名:
javascript
const callCounts = {};
function getSignature(call) {
return `${call.name}:${JSON.stringify(call.arguments)}`;
}每次执行工具前增加计数。如果同一个签名连续出现多次,可以中断循环或要求模型换一种方式:
javascript
if (callCounts[signature] >= 3) {
return '检测到重复动作,停止循环。';
}限制
上下文窗口
模型上下文窗口有限,消息历史增长到一定长度后必须截断或摘要。简单截断可能丢失早期工具结果,摘要则增加一次额外的模型调用。上下文管理是循环实现中必须处理的约束,而不是可选优化。
短期记忆与长期记忆
短期记忆是当前任务的 messages 数组,它承载模型在当前任务中的全部状态。长期记忆是跨任务存储的信息,通常放在外部数据库,例如 JSON 文件、SQLite 或向量数据库。在循环中,长期记忆通过工具暴露给模型:
javascript
registerTool('memory_store', '保存一条长期记忆', async ({ key, content }) => {
await db.set(key, content);
return 'ok';
});
registerTool('memory_load', '读取长期记忆', async ({ key }) => {
return await db.get(key);
});长期记忆让 Agent 在多次运行之间保持一致性,但它属于环境提供的资源,不属于循环本身。
失败恢复
循环中的状态不只有 messages,还包括当前计划、已执行步骤、预算剩余量等。把这些状态集中到一个对象中,便于失败恢复:
javascript
const state = {
step: 0,
cost: 0,
plan: [],
finished: false,
history: [],
};
state.history.push({
step: state.step,
reasoning: response.reasoning,
action: response.toolCalls,
observation: result,
});如果某一步因网络错误失败,可以从失败点重试;如果状态损坏,可以恢复到上一个检查点。这些机制不是循环的核心,但决定了循环能否在真实环境中稳定运行。
错误重试
模型 API 可能因网络波动、限流等原因失败。对瞬时错误,可以重试:
javascript
async function callWithRetry(fn, retries = 3) {
for (let i = 0; i < retries; i++) {
try {
return await fn();
} catch (err) {
if (i === retries - 1) throw err;
await new Promise((resolve) => setTimeout(resolve, 2 ** i * 100));
}
}
}工具本身的错误不需要重试,因为错误信息可能对模型有用。重试只适用于可恢复的传输层错误,而不是业务逻辑错误。
成本控制
成本控制可以从两个维度进行:
- 步数限制:限制最大循环轮数;
- token 预算:限制整个循环消耗的输入输出 token 总数。
在循环开始时初始化 totalTokens,每轮累加模型的用量,到达阈值后强制停止:
javascript
if (totalTokens > budget.maxTokens) {
return '预算耗尽,停止任务。';
}权限与安全护栏
工具调用具有真实影响,必须限制模型可访问的范围。基本手段是:
- 只注册必要工具;
- 在工具描述中说明权限限制;
- 危险操作需要人工确认。
人工确认可以在工具执行前插入一个检查点:
javascript
async function executeWithGuard(call) {
if (call.requiresApproval && !(await userConfirm(call))) {
return '用户取消了该操作。';
}
return executeTool(call);
}安全护栏不是循环的一部分,但缺少护栏的循环不能用于实际系统。
应用
ReAct:推理与行动交错
ReAct(Reason + Act)模式把推理和行动交错进行。每一步循环中,模型先输出思考,再输出一个动作,然后观察结果,再思考下一步。适用于需要探索和修正的任务。
ReAct 循环的对话流:
用户:北京适合跑步吗?
模型思考:需要天气数据。
模型动作:search("北京天气")
工具返回:北京多云,20°C,降水概率 10%
模型思考:降水概率低,适合。
模型回答:适合跑步。Plan-and-Execute:先规划后执行
Plan-and-Execute 模式先让模型生成完整计划,然后由执行器按计划调用工具。如果某个步骤失败,再触发模型重新规划。这种模式减少了模型调用次数,适合步骤相对固定的任务。
javascript
const plan = [
{ action: 'search', args: { query: '北京天气' } },
{ action: 'rule_eval', args: { threshold: 30 } },
{ action: 'answer', args: {} },
];执行器遍历 plan,依次执行。每一步的结果可以存入 stepResults 数组,供最后生成回答时使用。
模式选型对比
| 维度 | ReAct | Plan-and-Execute |
|---|---|---|
| 模型调用次数 | 每步都调用,较频繁 | 主要规划时调用,执行时少调用 |
| 适应变化的能力 | 高,可随时调整下一步 | 低,计划生成后较难调整 |
| 上下文消耗 | 每步的思考都进入上下文 | 执行阶段上下文相对固定 |
| 适用场景 | 探索性任务、信息不确定的任务 | 流程明确、步骤稳定的任务 |
两种模式经常混合使用:用 Plan-and-Execute 生成初始计划,执行过程中保留 ReAct 风格的错误修正。
日志与运行轨迹记录
记录每个阶段的输入输出,可以在问题定位和成本分析时使用。最小日志输出:
javascript
function logPhase(phase, data) {
console.log(JSON.stringify({ time: Date.now(), phase, data }));
}
logPhase('model_call', { step, messagesCount: messages.length });
logPhase('tool_call', { name: call.name, arguments: call.arguments });
logPhase('observation', { result });日志字段建议包含:会话 ID、步数、阶段、模型调用延迟、工具结果摘要。完整实现还会把日志写入独立存储,并和请求 ID 关联。
从单 Agent 循环到更复杂的 Agent 系统
LangChain 将 Agent 抽象为模型与 harness 的组合。harness 提供 prompt、工具和中间件。LangChain 和 LangGraph 还区分了 agent framework 与 agent runtime:
- Agent framework(如 LangChain、Vercel AI SDK、CrewAI、OpenAI Agents SDK、Google ADK、LlamaIndex)提供抽象层,例如结构化内容块、agent loop 和 middleware。
- Agent runtime(如 LangGraph、Temporal、Inngest)提供持久化、人工介入、事件流等能力。
选型时,快速构建 Agent、需要标准模型/工具/agent loop 抽象时适合用 agent framework;需要长期运行、有状态、复杂工作流编排时适合用 agent runtime。LangChain 1.0 构建在 LangGraph 之上,但使用 LangChain 不要求先掌握 LangGraph。
在更复杂的系统中,循环可能被扩展到以下方向:
- 多 Agent 之间传递状态;
- 通过数据库持久化循环状态;
- 由人类在关键步骤审批;
- 多个循环并行处理多个子任务。
评估执行循环
评估一个执行循环是否可用,可以关注以下指标:
- 任务完成率:目标是否达成;
- 平均步数:循环是否高效;
- token 消耗:成本是否可控;
- 失败恢复率:从错误中恢复的能力。
构造评估集时,应覆盖正常场景、缺少信息的场景、工具故障的场景和用户取消场景。例如,给 Agent 一个“天气查询”任务,但工具返回服务端错误,观察它是否能正确重试或澄清。
