【干货收藏】从零构建大模型Agent应用:TaoToken统一Key接入与MCP工具链实战指南
1. 从零构建大模型 Agent 应用TypeScript 技术栈下的完整链路大模型 Agent 应用不是「套个聊天框」那么简单。我在实际项目里踩过最深的坑是把 Agent 当成一个纯 LLM 调用来写结果工具调用乱飞、上下文爆炸、RAG 检索回来的内容跟当前任务八竿子打不着。后来才想明白一件事Agent 应用 Application Agent MCPApplication 是地基Agent 是大脑MCP 是手脚。地基不稳大脑再聪明也白搭。这篇内容聚焦 TypeScript 技术栈从零跑通一个可用的 Agent 应用。你会拿到三样东西一份可直接复制的settings.json与config.toml配置骨架、TaoToken 统一 Key 的接入步骤、以及 Agent 调用 MCP 工具的完整验证动作。中间会穿插 RAG 检索增强和 Context Engineering 上下文管理的实操细节。适合已经会写 TypeScript、但还没把 Agent 链路串起来的开发者。读完你至少能跑通一个「用户提问 → 任务拆分 → 工具调用 → 结果回传」的最小闭环。2. 前置准备TaoToken 统一 Key 与项目骨架2.1 为什么需要统一 Key 层Agent 应用通常要调用多个模型拆分任务用推理型、执行任务用代码型、RAG 重排可能又是另一个。如果每个模型都单独配一套 Key 和 endpoint配置文件会迅速失控。TaoToken 的做法是提供一个统一的 API 入口你只需要维护一个 Key就能在多个模型之间切换。官网地址https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 地址https://taotoken.net/api注意 API 地址不带 UTM 参数直接用于代码里的baseURL。2.2 获取 Key 与项目初始化先去控制台创建一个 API Keyhttps://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentconsole拿到 Key 之后初始化 TypeScript 项目mkdir agent-mcp-demo cd agent-mcp-demo npm init -y npm install typescript tsx types/node --save-dev npm install openai zod npx tsc --init --target ES2022 --module NodeNext --moduleResolution NodeNext这里用openaiSDK 作为客户端因为 TaoToken 的 API 兼容 OpenAI 协议格式换baseURL即可。zod用来做工具参数的 schema 校验后面 MCP 工具注册会用到。2.3 环境变量配置在项目根目录创建.envTAOTOKEN_API_KEYsk-你的Key TAOTOKEN_BASE_URLhttps://taotoken.net/api然后在src/config.ts里读取import OpenAI from openai; export const client new OpenAI({ apiKey: process.env.TAOTOKEN_API_KEY, baseURL: process.env.TAOTOKEN_BASE_URL, }); export const MODELS { splitter: claude-sonnet-4-20250514, executor: claude-sonnet-4-20250514, embedding: text-embedding-3-small, } as const;把模型名集中管理后面切换模型只改这一处。我试过在代码里散落写模型名改一次要全局搜索非常痛苦。3. 可复制配置settings.json 与 config.toml 骨架3.1 settings.jsonAgent 运行时配置这个文件定义 Agent 的行为边界、工具白名单和上下文窗口策略。放在项目根目录{ agent: { name: task-scheduler-agent, maxTurns: 12, contextWindow: 128000, reserveTokens: 8000, hitl: { enabled: true, confirmOnToolCall: true, confirmOnTaskSplit: true } }, tools: { registry: ./tools, extensions: [.mcp.ts], timeoutMs: 30000, maxConcurrent: 3 }, rag: { enabled: true, topK: 5, scoreThreshold: 0.72, chunkSize: 512, chunkOverlap: 64 }, models: { splitter: claude-sonnet-4-20250514, executor: claude-sonnet-4-20250514, embedding: text-embedding-3-small } }几个关键参数说明。maxTurns限制 Agent 最多循环多少轮防止死循环烧 token。reserveTokens是给系统提示词和工具定义预留的空间Context Engineering 的核心就是别让对话历史把窗口撑爆。hitl.confirmOnToolCall打开后每次工具调用前会暂停等用户确认这是保证可靠性的关键设计。3.2 config.tomlMCP 工具链配置MCP 工具的注册信息用 TOML 管理可读性比 JSON 好[mcp] version 1.0 transport stdio [[mcp.tools]] name read_task_list description 读取工作区中所有任务脚本的元信息 entry ./tools/read_task_list.mcp.ts requires_confirm false [[mcp.tools]] name write_task description 在工作区创建新的 TypeScript 任务脚本 entry ./tools/write_task.mcp.ts requires_confirm true [[mcp.tools]] name ripgrep_task description 在任务脚本内容中执行正则搜索 entry ./tools/ripgrep_task.mcp.ts requires_confirm false [mcp.limits] max_output_bytes 65536 allowed_paths [./workspace]requires_confirm控制单个工具是否需要人工确认。读操作可以放开写操作必须确认。allowed_paths做路径隔离防止 Agent 写到工作区外面去。3.3 工具定义文件示例以read_task_list.mcp.ts为例展示 MCP 工具的标准写法import { z } from zod; import fs from node:fs/promises; import path from node:path; export const schema z.object({ fullPath: z.string().describe(任务脚本的绝对路径), }); export const description 读取指定路径下任务脚本的元信息; export async function handler(args: z.infertypeof schema) { const content await fs.readFile(args.fullPath, utf-8); const lines content.split(\n).slice(0, 20); return { path: args.fullPath, preview: lines.join(\n), size: content.length, }; }这个文件同时导出了schema、description和handler。加载器会扫描./tools目录下所有.mcp.ts文件用ts-morph解析出这些导出自动生成给 LLM 看的工具定义 XML。你不需要手写工具描述改代码即改描述。4. 验证请求跑通 Agent 调用 MCP 工具4.1 加载工具并生成上下文先写一个工具加载器把.mcp.ts文件转成 LLM 能理解的格式import { glob } from node:fs/promises; import path from node:path; export async function loadTools(toolDir: string) { const tools []; for await (const file of glob(${toolDir}/*.mcp.ts)) { const mod await import(path.resolve(file)); tools.push({ name: path.basename(file, .mcp.ts), description: mod.description, schema: mod.schema, handler: mod.handler, }); } return tools; }然后把工具定义拼成 XML 塞进 System Prompt。根据 Claude Code 的实践经验LLM 对 XML 标签的理解比 JSON 更稳export function buildToolPrompt(tools: AwaitedReturnTypetypeof loadTools) { const toolXml tools .map( (t) tool name${t.name}/name description${t.description}/description parameters${JSON.stringify(t.schema.shape)}/parameters /tool ) .join(\n); return tools ${toolXml} /tools tool_calling 你有可用的工具来解决编码任务。遵循以下规则 1. 始终完全遵循指定的工具调用模式确保提供所有必需参数。 2. 在调用每个工具之前先向用户解释调用原因。 3. 一次回答中只能调用一次工具除非调用之间没有依赖关系。 4. 将调用工具的 invoke 标签放在整个回答的最末尾。 /tool_calling; }4.2 发起一次完整请求写一个最小可运行脚本src/run.tsimport dotenv/config; import { client, MODELS } from ./config.js; import { loadTools, buildToolPrompt } from ./tools-loader.js; async function main() { const tools await loadTools(./tools); const toolPrompt buildToolPrompt(tools); const response await client.chat.completions.create({ model: MODELS.executor, messages: [ { role: system, content: 你是一个任务调度平台的开发助手。 ${toolPrompt}, }, { role: user, content: 帮我创建一个每分钟采集 CPU 使用率的任务脚本, }, ], temperature: 0.2, }); console.log(response.choices[0].message.content); } main();运行npx tsx src/run.ts4.3 预期结果与工具调用解析正常输出应该包含思考过程和invoke标签类似我需要先查看工作区现有的任务脚本避免命名冲突。 invoke nameread_task_list/name reason读取工作区现有任务确认命名规范/reason params param namefullPath/name value./workspace/value /param /params /invoke解析这段 XML 的代码export function parseInvoke(text: string) { const match text.match(/invoke([\s\S]*?)\/invoke/); if (!match) return null; const name match[1].match(/name(.*?)\/name/)?.[1]; const reason match[1].match(/reason(.*?)\/reason/)?.[1]; const params: Recordstring, string {}; const paramRegex /param\s*name(.*?)\/name\s*value(.*?)\/value\s*\/param/g; let m; while ((m paramRegex.exec(match[1])) ! null) { params[m[1]] m[2]; } return { name, reason, params }; }拿到{ name, reason, params }之后去tools数组里找到对应的handler校验参数后执行把结果包成tool-call-result塞回对话历史继续下一轮。这就是 Agent 循环的核心。4.4 RAG 检索增强的接入点在工具执行结果回传之前可以加一层 RAG。比如ripgrep_task返回了 20 条匹配结果不要全塞给 LLM先用 embedding 做一次重排export async function rerank(query: string, docs: string[], topK 5) { const queryEmb await client.embeddings.create({ model: MODELS.embedding, input: query, }); const scored await Promise.all( docs.map(async (doc) { const docEmb await client.embeddings.create({ model: MODELS.embedding, input: doc, }); const score cosine(queryEmb.data[0].embedding, docEmb.data[0].embedding); return { doc, score }; }) ); return scored .filter((s) s.score 0.72) .sort((a, b) b.score - a.score) .slice(0, topK) .map((s) s.doc); }scoreThreshold和topK就是settings.json里配的那两个值。阈值设太低会引入噪声设太高会漏掉相关内容0.72 是我在几个项目里试出来的平衡点你可以根据实际语料调整。5. 本篇常见错排查5.1 工具加载报错Cannot find module症状loadTools执行时抛ERR_MODULE_NOT_FOUND。原因通常是tsconfig.json的moduleResolution没设成NodeNext导致.mcp.ts文件里的import路径解析失败。检查你的tsconfig.json{ compilerOptions: { target: ES2022, module: NodeNext, moduleResolution: NodeNext, strict: true } }另外动态import()的路径必须是绝对路径或带file://前缀。用path.resolve(file)包一层最稳妥。5.2 LLM 不输出invoke标签症状模型回复了文字但没有工具调用标签。三个排查方向。第一System Prompt 里tools标签的内容是否为空如果工具加载失败但没报错LLM 看不到任何工具自然不会调用。第二temperature是否设得太高超过 0.5 之后模型倾向于自由发挥建议工具调用场景设在 0.1 到 0.3。第三用户提问是否太模糊模型判断不需要工具就能回答。把提问改具体比如「读取 ./workspace 下的任务列表」而不是「帮我看看任务」。5.3 上下文窗口溢出症状请求报context_length_exceeded。这是 Context Engineering 没做好的典型表现。对话历史每轮都在增长工具返回的日志可能几千 token。解决方案是在每轮循环开始前做一次裁剪export function trimContext(messages: any[], maxTokens: number) { let total 0; const kept []; for (let i messages.length - 1; i 0; i--) { const len JSON.stringify(messages[i]).length / 4; if (total len maxTokens) break; total len; kept.unshift(messages[i]); } return kept; }保留最近的对话把早期的工具调用结果压缩成摘要。settings.json里的reserveTokens就是给这个操作留的余量。5.4 MCP 工具执行超时症状handler执行超过timeoutMs被中断。如果是ripgrep_task这类搜索工具大概率是搜索范围太大。在工具实现里加路径限制只搜allowed_paths配置的目录。如果是write_task写文件慢检查是不是写到了网络挂载盘。超时时间可以在settings.json的tools.timeoutMs里调但不建议超过 60 秒否则用户体验很差。6. 下一步把链路跑稳再谈优化到这里你已经有了一个能跑的最小闭环TaoToken 统一 Key 接入、MCP 工具注册、RAG 检索增强、Context Engineering 裁剪、HITL 人工确认。接下来要做的不是加更多功能而是把这条链路跑稳。几个我踩过坑之后总结的实用建议。第一工具描述一定要写清楚「什么时候用」和「什么时候不用」LLM 对否定约束的遵循度比你想的高。第二每次工具调用结果回传时在tool-call-result里带上执行耗时和状态码方便排查问题。第三HITL 的确认按钮不要做成「全部确认」要支持单条任务确认否则用户会烦。如果你在接入过程中遇到 API 报错或工具注册失败可以先查接入文档https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentdoc想先验证模型对话是否正常用模型对话页面发一条测试消息https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentmodel如果你打算长期做编码类 Agent或者要跑多轮工具调用的复杂任务Coding Plan 的额度模型比按量计费更划算https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentcoding-planKey 管理和用量查看在控制台https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentconsoleAPI Key 创建入口https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentapi-keys最后说一个细节。Claude Code 的 Anthropic 兼容接入方式在 TaoToken 上也能用如果你习惯用 Claude Code 做开发可以直接把 endpoint 指过来https://taotoken.net/claude-code-anthropic?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentclaudecode把上面这套配置跑通之后你会发现 Agent 应用的难点从来不是模型不够聪明而是 Application 层能不能给 Agent 提供稳定、可预期、有边界的工具和上下文。MCP 工具链的质量直接决定了 Agent 的上限。