AI代理的本质:循环调用LLM
“AI代理”听起来很复杂,但通常核心比你想象的要简单:AI代理通常在一个循环中返回调用大型语言模型(LLM)。
“AI代理”听起来很复杂,但通常核心比你想象的要简单。AI代理通常在一个循环中运行大型语言模型(LLM)。
这段伪代码用JavaScript捕捉了本质:
// 环境保存状态/上下文
const env = { state: initialState }; // 简单对象状态
// 代理可用的工具
const tools = new Tools(env);
// LLM的指令
const system_prompt = `你的主要任务、目标、限制...`;
// 主代理循环
while (true) {
// (需要一个真正的退出条件!)
// 1. LLM大脑:根据提示+状态决定动作
const action = llm.run(`${system_prompt} ${env.state}`);
// 2. 系统双手:运行请求的工具的实际代码
env.state = tools.run(action); // 用结果更新状态
}
这个简单的循环是代理的核心。

快速分解:
llm.run(...): “大脑”。使用指令(system_prompt) + 当前情况(env.state)来决定下一步的action。tools.run(action): “双手”。如果action请求了一个工具,这会执行该工具的实际代码并更新env.state。
循环重复,将新状态反馈给LLM。
1、为什么需要工具?
因为LLM只会说话。
LLM只输出文本。它不能直接做事情 – 没有浏览、没有文件编辑、没有运行命令。它需要“双手”。
工具就是那些双手。它们是系统在被要求时为LLM执行的函数,使它能够与世界互动。
2、定义和使用一个工具
LLM如何知道工具是什么以及何时使用它们?
2.1 工具定义
每个工具需要传递给LLM的清晰定义,详细说明:
- 名称: 唯一ID(例如,
runLinter)。 - 描述: 工具是什么以及做什么(例如,“在JS代码/文件上运行ESLint...”)。
- 输入参数: 需要的确切输入(每个的名称、类型、描述)。
这个定义告诉LLM工具的能力以及如何为其结构化请求。
// 传递给LLM API的示例工具定义
{
type: "function",
function: {
name: "runLinter", // 唯一名称
description: "在JS代码/文件上运行ESLint,返回JSON错误或成功消息。", // 描述
parameters: { /* 输入参数模式 */ }
}
}

2.2 何时使用工具:系统提示
主要的system_prompt为代理提供了核心指令和策略。
至关重要的是,它告诉LLM何时和如何使用它的工具。它列出了可用的工具并设定了使用规则。
示例: “你有runLinter工具。始终首先运行它... 如果发现错误,修复代码,然后再次运行runLinter以验证,然后再完成。”
这确保了LLM在循环内有效地使用工具。
3、动手实践:构建一个简单的lint代理
让我们看看它如何运作。我们将逐步介绍一个简单的、可工作的Node.js代理的关键部分,该代理使用一个单一工具来修复JavaScript linting错误。
你可以在这里找到完整、可运行的代码(包括完整的系统提示)。
以下是关键部分:
3.1 系统提示(src/config.js - 缩略版)
此片段展示了代理编程的结构和关键指令。
// src/config.js - 系统提示(缩略版)
const config = {
model: "gpt-4o",
systemPrompt: `
你是一个帮助修复linting错误的专家JavaScript助手...
可用工具:
- runLinter({ codeContent?: string, filePath?: string }): 执行ESLint...
遵循的流程:
1. 接收代码/路径。
2. **始终**首先使用 'runLinter'...
3. 分析错误...
4. 如果没有错误,返回代码...
5. 如果有错误:
a. 修改代码...
b. **重要:** 再次调用 'runLinter' 以验证...
c. 如果验证通过,返回更正后的代码...
d. 如果仍然有错误,在尝试后返回最佳努力...
最终响应:
你的最终响应必须只包含完整的更正后的代码...严格包裹在 <final_code>...</final_code> 中...
// (存储库代码中的工具调用和最终响应示例的完整细节)
`,
};
export default config;
3.2 工具(src/tools/linter.js - 函数片段)
这显示了系统执行的实际 runLinter 函数的核心逻辑,省略了一些样板代码以提高清晰度。
// src/tools/linter.js - 核心工具函数(简化版)
import { ESLint } from "eslint";
import fs from "fs"; // 仍需要用于上下文
const runLinter = async ({ codeContent, filePath }) => {
// --- 确定代码来源和处理临时文件逻辑 ---
// ... 获取 codeToLint 和管理 useFilePath 的代码 ...
// ... 包括 fs.readFileSync/writeFileSync 逻辑 ...
try {
// --- 核心 ESLint 执行 ---
const eslint = new ESLint({ fix: false, useEslintrc: true });
const results = await eslint.lintFiles([
/* 确定的文件路径 */
]);
// --- 处理结果 ---
const errors = results.flatMap(/* ... 将结果映射到错误对象 ... */);
// --- 清理和返回 ---
// ... 如果使用了临时文件则删除 ...
return errors.length === 0
? { result: "未找到任何 linting 错误!" }
: { errors: JSON.stringify(errors) };
} catch (err) {
// --- 错误处理和清理 ---
// ... 删除临时文件 ...
console.error("运行 ESLint 时出错:", err);
return { error: `ESLint 执行错误: ${err.message}` };
}
};
// --- 导出的定义供代理/LLM 使用 ---
export default {
name: "runLinter",
description: "在 JS 代码/文件上运行 ESLint...",
parameters: {
/* ... 参数模式 ... */
},
function: runLinter, // 链接到上面的实际函数
};
3.3 循环(src/agent/agentInvoker.js - 核心逻辑片段)
此片段突出了代理的执行流程:调用LLM,处理工具调用,并更新状态。
// 在 src/agent/agentInvoker.js 中(简化的核心循环)
import linterTool from "../tools/linter.js";
import config from "../config.js";
import memory from "./memory.js";
// ... 其他函数: callLLM(messages), handleToolCall(toolCall) ...
const tools = [/* ... 使用 linterTool 的工具定义结构 ... */];
const invokeAgent = async (conversationId, inputs) => {
// ... 设置初始用户消息在 memory 中 ...
const MAX_ITERATIONS = 3;
let finalCode = null;
// === 代理循环 ===
for (let i = 0; i < MAX_ITERATIONS; i++) {
const messages = memory.getMessages(conversationId); // 获取状态
// --- 1. LLM 大脑:决定动作 ---
const llmResponse = await callLLM(messages);
const assistantMessage = llmResponse.choices[0].message;
memory.addMessage(conversationId, assistantMessage); // 存储想法
if (assistantMessage.tool_calls) {
// --- 2. 系统双手:执行工具 ---
for (const toolCall of assistantMessage.tool_calls) {
if (toolCall.function.name === linterTool.name) {
const toolResult = await handleToolCall(toolCall); // 运行 runLinter
const toolResultContent = /* ... 格式化结果 ... */ ;
// --- 3. 更新状态 ---
memory.addMessage(conversationId, { role: "tool", /*...*/ content: toolResultContent });
}
}
} else if (assistantMessage.content) {
// --- LLM 提供了最终答案 ---
const match = /* ... 检查 <final_code> ... */ ;
if (match) { finalCode = match[0]; break; } // 目标达成!
} else { /* ... 处理错误 ... */ break; }
} // === 结束循环 ===
// ... 处理循环结束 ...
return finalCode;
};
这种结构展示了LLM(规划) + 循环(重复) + 工具(行动) 的模式。
4、连接到更复杂的代理(如 Cursor)
我们的简单 linter 代理只使用了一个工具,但它展示了基本模式。现实世界的代理如 Cursor 操作在完全相同的原则上,只是扩展了。
想象一下,你让 Cursor 来“重构 ComponentA.jsx 以使用新的 useDataFetching hook 并在 ComponentA.test.js 中更新其测试。” Cursor 的 LLM 大脑,由其自己的复杂系统提示引导,可能在其循环中协调如下序列:
- 循环 1:
LLM思考:“需要ComponentA.jsx。” -> 动作: 调用readFile(path="...")。系统运行它。 - 循环 2:
LLM思考:“需要ComponentA.test.js。” -> 动作: 调用readFile(path="...")。系统运行它。 - 循环 3:
LLM思考:“计划 JSX 更改。” -> 动作: 调用editFile(path="...", changes=[...])。系统运行它。 - 循环 4:
LLM思考:“计划测试更改。” -> 动作: 调用editFile(path="...", changes=[...])。系统运行它。 - 循环 5:
LLM思考:“验证更改。” -> 动作: 调用runTests(path="...")。系统运行它。 - 循环 N: (继续...)
这是相同的思考 -> 行动(工具)-> 更新状态 -> 重复 周期,只是有更多的工具(readFile, editFile, runTests 等)和更复杂的策略。核心的LLM + 循环 + 工具 架构保持不变。
5、实用的结论
忘记关于“AI代理”的复杂炒作。核心通常是这种简单的LLM + 循环 + 工具模式:
- LLM 思考(使用系统提示 + 工具定义 + 当前状态)
- 系统行动(运行请求的工具的实际代码)
- 重复
这是一种简单而强大的方式,使 LLM 完成现实世界中的任务。
原文链接:Forget the Hype: Agents are Loops
汇智网翻译整理,转载请标明出处