
1. 从零构建 AI Agent 到底难在哪LLM、Tool Calling 与 Agent Loop 的真实卡点很多人第一次听到「AI Agent」这个词脑子里浮现的是科幻电影里那种能自己思考、自己干活的数字员工。但真动手写代码时往往卡在第一步我到底该从哪里开始是先把大模型 API 调通还是先写工具函数为什么我的 Agent 调了一次工具就停了不会自己继续往下走这些问题的根源在于大多数人把 Agent 当成一个「更聪明的聊天机器人」来理解而实际上 Agent 是一套架构模式。它的核心公式可以拆成三块Agent LLM推理大脑 Tools执行手脚 Loop驱动循环LLM 负责理解任务、做决策Tools 负责执行具体操作Loop 让 AI 反复「思考 → 行动 → 观察」直到任务完成。少了任何一块它都只是个半成品。我见过太多人卡在中间某一环有人 API 调通了但不知道怎么让模型返回结构化的工具调用请求有人工具写好了但不知道 Agent Loop 该怎么写才不会死循环还有人工具越接越多每个都要写一套定义、解析、执行的胶水代码维护成本直接爆炸。这篇文章就是来解决这些具体问题的。我会带你从最基础的单次 API 调用开始一步步搭到能自主运行的 Agent Loop再讲清楚 MCP 协议怎么把工具对接从 M×N 降到 MN最后给出一套可复制的配置和验证步骤。全程用 TaoToken 统一 Key 接入省去你在多个厂商之间来回切换的麻烦。适合谁看有基础编程能力、想从零跑通一个最小可用 Agent 的开发者已经会调 LLM API 但没搞明白 Tool Calling 和 Agent Loop 怎么串起来的同学以及想用 MCP 扩展工具生态但不知道从哪下手的人。2. TaoToken 前置准备统一 Key 接入 LLM 与 Tool Calling 的配置指南在开始写 Agent 代码之前先把「接入层」搞定。这一步的核心目标是用一个 Key、一个 Base URL就能调用不同厂商的模型这样后面写 Agent Loop 时不用关心底层换的是哪家模型。2.1 为什么需要统一 Key如果你直接对接各家厂商代码里会散落着不同的 endpoint、不同的鉴权方式、不同的请求格式。一旦想换个模型测试效果就得改一堆配置。TaoToken 的做法是提供一个兼容 OpenAI 接口规范的统一入口你只需要维护一份配置Base URLhttps://taotoken.net/apiAPI Key在控制台生成Model ID按需选择具体模型这样你的 Agent 代码里只需要一个client换模型只改一个字符串。2.2 获取 API Key打开 TaoToken 控制台进入 API Keys 页面创建一个新的 Key。建议按用途命名比如agent-dev方便后续排查问题时定位。注意Key 只在创建时完整显示一次记得立刻复制保存。如果泄露了直接在控制台删除重建即可。2.3 可复制的配置文件下面这份配置可以直接用。我以 Node.js 项目为例把 Base URL、Key、Model ID 三件套写进.env文件# .env TAOTOKEN_BASE_URLhttps://taotoken.net/api TAOTOKEN_API_KEYsk-你的实际Key TAOTOKEN_MODEL_IDclaude-sonnet-4-20250514如果你用的是 Claude Code 或类似的 CLI 工具配置方式略有不同。以 Claude Code 的 settings 为例需要指定ANTHROPIC_BASE_URL和ANTHROPIC_AUTH_TOKEN{ env: { ANTHROPIC_BASE_URL: https://taotoken.net/api, ANTHROPIC_AUTH_TOKEN: sk-你的实际Key, ANTHROPIC_MODEL: claude-sonnet-4-20250514 } }如果你用的是 Codex 这类工具配置写在auth.json里{ base_url: https://taotoken.net/api, api_key: sk-你的实际Key, model: claude-sonnet-4-20250514 }三件套的核心逻辑是一致的Base URL 指向 TaoToken 的 API 入口Key 用于鉴权Model ID 决定实际调用哪个模型。不管你用哪种工具只要这三项填对接入就不会有问题。2.4 验证接入是否成功在写 Agent 之前先用一个最简单的请求确认接入层是通的// verify-connection.js const response await fetch(https://taotoken.net/api/v1/chat/completions, { method: POST, headers: { Content-Type: application/json, Authorization: Bearer ${process.env.TAOTOKEN_API_KEY} }, body: JSON.stringify({ model: process.env.TAOTOKEN_MODEL_ID, messages: [ { role: user, content: 回复一个字通 } ] }) }); const data await response.json(); console.log(data.choices[0].message.content);跑通这个请求说明你的 Key、Base URL、Model ID 都没问题。接下来就可以在这个基础上叠加 Tool Calling 和 Agent Loop 了。3. 可复制配置Tool Calling 工具注册与 Agent Loop 循环代码这一节是整篇文章的核心。我会给出完整的工具注册格式、Agent Loop 伪代码以及如何把 TaoToken 的统一 Key 接进去。3.1 工具注册表的标准格式Tool Calling 的关键在于你要用模型能理解的格式描述每个工具。下面是一个标准的工具定义// tools/registry.js const toolRegistry { run_shell_command: { name: run_shell_command, description: 在用户机器上执行 shell 命令并返回输出, parameters: { type: object, properties: { command: { type: string, description: 要执行的 shell 命令 } }, required: [command] }, execute: async (params) { const { command } params; return new Promise((resolve) { exec(command, { timeout: 10000 }, (error, stdout, stderr) { resolve({ exitCode: error?.code ?? 0, stdout: stdout.trim(), stderr: stderr.trim() }); }); }); } }, read_file: { name: read_file, description: 读取指定文件的内容支持分页, parameters: { type: object, properties: { path: { type: string, description: 文件路径 }, startLine: { type: number, description: 起始行号 }, endLine: { type: number, description: 结束行号 } }, required: [path] }, execute: async (params) { const content await fs.readFile(params.path, utf-8); const lines content.split(\n); const start (params.startLine ?? 1) - 1; const end params.endLine ?? lines.length; return { path: params.path, totalLines: lines.length, content: lines.slice(start, end).join(\n) }; } } };每个工具包含四个部分name唯一标识、description给模型看的说明、parametersJSON Schema 格式的参数定义、execute实际执行函数。3.2 Agent Loop 核心循环有了工具注册表接下来写 Agent Loop。这是让 Agent 从「单步执行」进化到「自主多步」的关键// agent/loop.js class Agent { constructor(client, toolRegistry, systemPrompt) { this.client client; this.toolRegistry toolRegistry; this.systemPrompt systemPrompt; this.history []; this.maxRounds 10; } async send(userInput) { this.history.push({ role: user, content: userInput }); for (let round 0; round this.maxRounds; round) { const response await this.client.chat({ model: process.env.TAOTOKEN_MODEL_ID, messages: [ { role: system, content: this.systemPrompt }, ...this.history ], tools: Object.values(this.toolRegistry).map(t ({ type: function, function: { name: t.name, description: t.description, parameters: t.parameters } })) }); const message response.choices[0].message; this.history.push(message); // 如果没有工具调用说明任务完成 if (!message.tool_calls || message.tool_calls.length 0) { return message.content; } // 执行所有工具调用 for (const toolCall of message.tool_calls) { const toolName toolCall.function.name; const args JSON.parse(toolCall.function.arguments); const tool this.toolRegistry[toolName]; if (!tool) { this.history.push({ role: tool, tool_call_id: toolCall.id, content: JSON.stringify({ error: 未知工具: ${toolName} }) }); continue; } const result await tool.execute(args); this.history.push({ role: tool, tool_call_id: toolCall.id, content: JSON.stringify(result) }); } } throw new Error(超过最大工具调用轮数可能存在死循环); } }这段代码的核心逻辑是每轮把完整历史 工具列表发给模型如果模型返回 tool_calls 就执行工具并把结果塞回历史然后进入下一轮如果模型返回纯文本说明任务结束。maxRounds是安全阀防止模型陷入死循环。实际项目中建议设成 10 到 15 之间。3.3 接入 TaoToken 统一 Key把上面的 Agent 类和 TaoToken 的接入配置串起来// index.js import OpenAI from openai; const client new OpenAI({ baseURL: process.env.TAOTOKEN_BASE_URL, apiKey: process.env.TAOTOKEN_API_KEY }); const agent new Agent( { chat: (params) client.chat.completions.create(params) }, toolRegistry, 你是一个交互式 CLI Agent。禁止废话直接输出结果。调用工具前先做简短推理。 ); const result await agent.send(帮我看看当前目录有什么文件然后统计 .js 文件的行数); console.log(result);这里的关键是baseURL指向 TaoToken 的 API 入口apiKey用你在控制台生成的 Key。模型 ID 通过TAOTOKEN_MODEL_ID环境变量传入换模型时只改这一个值。4. 验证请求与成功结果一轮端到端 Agent 调用实测配置写完了接下来跑一轮完整的端到端调用看看 Agent 是不是真的能自主完成多步任务。4.1 测试任务设计我设计了一个需要多步工具调用的任务「帮我看看当前目录有什么文件然后统计 .js 文件的行数」这个任务需要 Agent 至少执行两步先列目录再统计行数。如果 Agent Loop 写对了它应该能自动完成这两步并给出总结。4.2 实际运行过程启动 Agent 后控制台输出如下[Round 1] 模型返回 tool_calls: → run_shell_command({ command: ls -la }) 执行结果: { exitCode: 0, stdout: total 48\ndrwxr-xr-x ... index.js\nagent.js\ntools.js, stderr: } [Round 2] 模型返回 tool_calls: → run_shell_command({ command: wc -l *.js }) 执行结果: { exitCode: 0, stdout: 120 index.js\n 85 agent.js\n 60 tools.js\n 265 total, stderr: } [Round 3] 模型返回纯文本: 当前目录共有 3 个 .js 文件总行数 265 行。其中 index.js 120 行agent.js 85 行tools.js 60 行。三轮循环Agent 自主完成了「列目录 → 统计行数 → 总结」的完整流程。这就是 Agent Loop 的价值你不需要告诉它每一步该做什么它自己会规划。4.3 成功结果的关键指标判断 Agent 是否跑通看三个指标第一模型是否返回了结构化的tool_calls。如果返回的是纯文本说明工具定义没传对或者模型不支持 Tool Calling。第二工具执行结果是否正确回传。每轮工具执行后结果要以role: tool的消息塞回历史并且带上对应的tool_call_id。第三循环是否在合理轮数内终止。如果超过maxRounds还没结束要么是任务太复杂需要拆分要么是工具设计有问题导致模型反复调用同一个工具。4.4 用模型对话快速验证如果你不想写代码也可以直接在 TaoToken 的模型对话页面测试 Tool Calling 的效果。把工具定义粘贴进去看模型是否能正确返回调用请求。这是验证工具描述是否清晰的最快方式。5. 本篇常见错排查401、local proxy failed 与 reading choices 报错解决这一节整理我在搭建过程中实际踩过的坑以及对应的排查思路。5.1 401 Unauthorized报错信息{ error: { message: Invalid API key provided, type: invalid_request_error, code: invalid_api_key } }原因Key 填错了、Key 被删了、或者环境变量没加载到。排查步骤第一检查.env文件里的TAOTOKEN_API_KEY是否和控台生成的一致。注意不要有多余的空格或换行。第二确认代码里读取环境变量的方式正确。Node.js 项目需要dotenv加载.env文件否则process.env.TAOTOKEN_API_KEY是undefined。第三如果用的是 Claude Code 或 Codex检查 settings 或 auth.json 里的字段名是否正确。Claude Code 用的是ANTHROPIC_AUTH_TOKEN不是ANTHROPIC_API_KEY。5.2 local proxy failed报错信息Error: local proxy failed: connect ECONNREFUSED 127.0.0.1:7890原因代码或工具里配置了本地代理但代理服务没启动。排查步骤第一检查环境变量里是否有HTTP_PROXY或HTTPS_PROXY指向本地端口。如果有确认对应的代理服务是否在运行。第二如果不需要代理直接把这些环境变量清掉unset HTTP_PROXY unset HTTPS_PROXY第三检查代码里是否硬编码了代理地址。有些 HTTP 客户端库会读取系统代理设置需要显式禁用。5.3 reading choices 报错报错信息TypeError: Cannot read properties of undefined (reading choices)原因API 返回的响应结构不符合预期通常是请求本身失败了但代码没有检查错误就直接读data.choices。排查步骤第一在读取choices之前先打印完整响应const data await response.json(); console.log(JSON.stringify(data, null, 2));第二检查response.ok是否为true。如果不是说明 HTTP 状态码不是 200需要看data.error里的具体信息。第三确认请求体格式正确。messages必须是数组model必须是字符串tools的格式要符合 OpenAI 规范。5.4 OAuth 相关报错报错信息Error: OAuth token expired or invalid原因如果你用的是需要 OAuth 认证的工具比如某些 CLItoken 过期了。排查步骤第一重新执行登录流程获取新的 token。第二检查系统时间是否准确。OAuth token 通常有有效期系统时间偏差过大会导致 token 被判定为过期。第三如果工具支持 API Key 认证优先用 API Key 替代 OAuth配置更简单也更稳定。5.5 工具调用返回空结果现象模型返回了tool_calls但执行后模型没有继续下一步直接返回了空文本。原因工具执行结果没有正确回传或者tool_call_id不匹配。排查步骤第一确认每条role: tool的消息都带了tool_call_id且和模型返回的id一致。第二确认工具执行结果被序列化成了字符串。有些模型要求content必须是字符串不能直接传对象。第三检查工具执行是否抛出了异常。如果execute函数报错但没有被捕获整个循环会中断。6. 语义一致 CTA从最小 Agent 到生产级 Coding Agent 的下一步跑通最小可用 Agent 之后下一步通常是把它变成一个真正能日常使用的 Coding Agent。这里有几个方向可以继续深入。方向一用 MCP 扩展工具生态。手动写工具定义的方式在工具数量少时没问题但一旦超过十个维护成本就会急剧上升。MCP 协议把工具对接标准化你只需要启动对应的 MCP ServerAgent 就能自动发现并调用工具。想了解 MCP 的具体接入方式可以看接入文档。方向二优化上下文管理。Agent 跑久了历史消息会塞满上下文窗口。滑动窗口、摘要压缩、RAG 检索这三种策略可以组合使用。我的经验是短期对话用滑动窗口长期任务用摘要压缩知识密集型场景加 RAG。方向三引入 Sub-Agent 分工。单 Agent 什么都干容易上下文污染。把调研、编码、审查拆成独立的 Sub-Agent每个有自己的上下文和工具白名单主 Agent 只负责调度和汇总。方向四用 Skill 封装固定流程。像「生成 commit message」这种每次步骤一样的任务没必要让模型每次重新规划。把它封装成 Skill一条命令触发流程固定结果稳定。如果你还没有 API Key可以先去控制台创建一个。想先体验模型对话和 Tool Calling 的效果可以直接在模型对话页面测试。需要长期跑 Coding Agent 的话Coding Plan 提供了更稳定的调用额度。Agent 这个领域变化很快但底层逻辑是稳定的LLM 负责推理Tools 负责执行Loop 负责驱动。把这三块吃透上层怎么变都能接得住。