AI代理的本质:循环调用LLM

“AI代理”听起来很复杂,但通常核心比你想象的要简单:AI代理通常在一个循环中返回调用大型语言模型(LLM)。

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: { /* 输入参数模式 */ }
  }
}

用名称、描述和参数为LLM定义一个工具

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. 循环 1: LLM 思考:“需要 ComponentA.jsx。” -> 动作: 调用 readFile(path="...")。系统运行它。
  2. 循环 2: LLM 思考:“需要 ComponentA.test.js。” -> 动作: 调用 readFile(path="...")。系统运行它。
  3. 循环 3: LLM 思考:“计划 JSX 更改。” -> 动作: 调用 editFile(path="...", changes=[...])。系统运行它。
  4. 循环 4: LLM 思考:“计划测试更改。” -> 动作: 调用 editFile(path="...", changes=[...])。系统运行它。
  5. 循环 5: LLM 思考:“验证更改。” -> 动作: 调用 runTests(path="...")。系统运行它。
  6. 循环 N: (继续...)

这是相同的思考 -> 行动(工具)-> 更新状态 -> 重复 周期,只是有更多的工具(readFile, editFile, runTests 等)和更复杂的策略。核心的LLM + 循环 + 工具 架构保持不变。

5、实用的结论

忘记关于“AI代理”的复杂炒作。核心通常是这种简单的LLM + 循环 + 工具模式:

  1. LLM 思考(使用系统提示 + 工具定义 + 当前状态)
  2. 系统行动(运行请求的工具的实际代码)
  3. 重复

这是一种简单而强大的方式,使 LLM 完成现实世界中的任务。


原文链接:Forget the Hype: Agents are Loops

汇智网翻译整理,转载请标明出处