1. Agent 核心实现 - Agent Loop

Agent 和 Chat 的区别

维度ChatAgent
交互模式问答,用户驱动自主,目标驱动
能力边界生成文本、图片、视频等可以通过调用工具,影响真实世界
执行流程用户提问 模型回答用户下达任务 思考 调用工具 观察 思考 返回答案
状态管理每轮对话独立维护完整的消息历史,包含工具调用与返回结果
自主性模型自主决定“下一步”,“使用什么工具”,“何时停止”

Agent 核心架构

1. LLM 客户端初始化
2. 工具定义(Tool Shcema)
3. 工具实现(Tool Functions)
4. Agent 循环(Core Loop)

Agent 循环的执行顺序

具体举例:

  1. 用户输入“统计当前文件夹文件数,新建并写入到count.txt”
  2. [第一轮循环]发送给 LLM,LLM返回的不是普通文本,而是一个tool_call,请求调用工具
  3. 收到 LLM 的工具调用请求,本地调用工具
  4. 追加调用的结果到 messages
  5. [第二轮循环] 发送给LLM,LLM收到了调用的文件数结果,决定写入文件,返回一个tool_call
  6. 收到 LLM 的工具调用请求,本地调用工具
  7. 追加调用的结果到 messages
  8. [第三轮循环] 发送给LLM,LLM收到调用结果是成功,判断任务完成,返回纯文本
  9. 退出循环,用户收到LLM的创建成功的消息

import { OpenAI } from "openai";
import { ChatCompletionMessage } from "openai/resources/index";
import { tools, toolsMap } from "./tools";
 
type ChatMessage = OpenAI.Chat.Completions.ChatCompletionMessageParam;
 
/**
 * 调用 LLM
 * @param messages
 * @returns
 */
async function callLLM(
  messages: ChatMessage[],
): Promise<ChatCompletionMessage> {
  const config = new Config();
  const { apiKey, baseURL, model } = config;
  if (!apiKey) {
    console.error("Error: OPENAI_API_KEY environment variable is required");
    process.exit(1);
  }
  const client = new OpenAI({
    apiKey,
    baseURL,
  });
  const resp = await client.chat.completions.create({
    model,
    messages,
    tools,
  });
  return resp.choices[0]?.message;
}
 
/**
 * 核心代码,实现AgentLoop,通过循环让大模型持续使用工具
 * @param userInput
 * @param maxIterations
 * @returns
 */
async function agentLoop(userInput: string, maxIterations = 20) {
  const messages: ChatMessage[] = [
    {
      role: "system",
      content:
        "You are a helpful assistant that can interact with the system using bash commands. Be concise and helpful.",
    },
    {
      role: "user",
      content: userInput,
    },
  ];
  for (let i = 0; i < maxIterations; i++) {
    console.log(`\n[Iteration ${i + 1}/${maxIterations}]`);
    const msg = await callLLM(messages);
    messages.push(msg);
    if (msg.content) {
      console.log(`\nAssistant: ${msg.content}`);
    }
    if (msg?.tool_calls?.length) {
      for (const toolCall of msg.tool_calls) {
        if (toolCall.type === "function") {
          const funcName: string = toolCall.function.name;
          const args = JSON.parse(toolCall.function.arguments || "{}");
          console.log(`\n[Tool Call] ${funcName}:`, args);
          let toolOutput = "";
          if (typeof toolsMap[funcName] === "function") {
            toolOutput = await toolsMap[funcName](args.command);
            console.log(`[Output]:\n${toolOutput}`);
          } else {
            toolOutput = `Unknown tool: ${funcName}`;
          }
          messages.push({
            role: "tool",
            tool_call_id: toolCall.id,
            content: toolOutput,
          });
        }
      }
    } else {
      // No more tool calls, we're done
      break;
    }
  }
  return messages;
}
 
async function main() {
  console.log("starting");
  const args = process.argv.slice(2);
  const userInput =
    args.length > 0 ? args.join(" ") : "List files in current directory";
 
  console.log("=== Any-Agent-Nano ===");
  console.log(`User: ${userInput}\n`);
 
  await agentLoop(userInput);
}
 
main().catch(console.error);
 
export default main;
 

核心工具

  • Bash: 理论上有了 Bash,LLM可以实现所有东西,但是 如果不指明方向,LLM会过度发散,比如让它编辑一个文件,它甚至会使用 Python 来编辑。
  • Read: 读取文件,是 LLM 开始编程的基石
  • Write: 写入文件,读取 + 写入,就可以实现一个 code agent 原型了,但是当文件内容比较多的时候,覆盖写入会导致上下文过长
  • Update/Edit: 编辑文件,部分写入可以优先减少上下文的长度,配合Diff,就可以实现新旧版本对比

设计理解

  1. 为什么设置 maxIterations? 这是一个安全阈值,防止LLM陷入死循环,确保程序最终能够停下来,在生产环境中,这个值通常更大,而且会配置多种复杂的执行条件。
  2. LLM 如何做出决定调用工具的? LLM并没有执行代码或者调用函数,实际上是我们传入了tools参数,LLM阅读了tools参数后,在适当的时候输出一段JSON,描述自己想要调用哪个函数,传入什么参数。我们收到这段JSON后,在本地执行真正的函数,再把结果返回给LLM。LLM是“大脑”,负责思考,tools和对应但函数是手脚,负责执行。

从实现核心,看 Agent 到底是什么?

从实现核心我们可以看出 Agent 的三个本质要素:

  1. 感知 - 通过工具获取外部信息
  2. 决策 - LLM 根据用户输入和已有的工具信息,决定下一步决策
  3. 行动 - LLM 通过工具调用,影响外部环境

无论是OpenClaw、Claude Code 还是 OpenCode都遵循这个范式。当 OpenClaw 在创建定时任务、网页搜索、修改本地文件的时候,背后就是这样的 Agent Loop 在驱动。