ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Claude Code源码泄漏后,用TaoToken拆解工业级AI Coding Agent的TypeScript+React架构

Claude Code源码泄漏后,用TaoToken拆解工业级AI Coding Agent的TypeScript+React架构 1. 从源码泄漏事件说起工业级 AI Coding Agent 到底难在哪Claude Code 源码泄漏这件事在开发者圈子里炸开锅的原因不是能看到 Anthropic 内部代码这种猎奇心理而是它第一次把工业级 AI Coding Agent的完整工程实现摊开在所有人面前。很多人以为这类工具就是调个 LLM API 包一层命令行看完源码才发现它本质上是一个跑在终端里的 Agent 操作系统TypeScript React(Ink) 撑起了整个骨架代码量远超想象。我自己第一次翻这套结构时最直观的感受是难点从来不在让模型写代码而在让模型安全、稳定、可扩展地写代码。一个能跑通的 demo可能 200 行就够但要做到工业级你要处理的是流式工具执行、并行调度、多阶段权限校验、上下文自动压缩、多 Agent 协作、后台记忆管线、插件扩展体系……每一项单独拎出来都是硬骨头。这篇文章不打算复述泄漏代码的八卦而是从TypeScript 与 React 技术栈切入把工业级 AI Coding Agent 的模块分层和 Agent 调度机制拆开讲清楚。更重要的是我会给出可复制的项目结构配置和本地验证步骤让你不只是看懂而是能动手复现关键链路。如果你正在造自己的 Coding Agent或者想理解这类工具为什么这么设计这篇值得从头看到尾。适合谁看有 TypeScript 基础、想深入 Agent 架构的中高级开发者正在做 AI 编程工具、需要参考工业级实现的团队以及想搞清楚Agent 循环到底怎么跑的技术爱好者。全程不需要你懂 Bun 或 Ink 的细节我会用类比把关键机制讲透。2. 前置准备用 TaoToken 打通模型调用链路在动手复现 Agent 架构之前你得先有一个能稳定调用的模型入口。工业级 Agent 的核心循环是 ReActQuery → LLM → ToolUse → ToolResult → LLM → ...这条链路里 LLM 调用是最高频的一环如果每次调试都要折腾鉴权和网络根本没法专注在架构本身。我实测下来用TaoToken作为模型接入层是比较省心的选择。它提供统一的 API 入口兼容主流模型调用格式你不需要在本地维护一堆环境变量和鉴权逻辑把 Base URL 和 Key 配好就能直接跑通 Agent 循环。官网地址是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 入口是 https://taotoken.net/api 。这里要强调一个概念TaoToken 不是中转而是标准的模型调用服务。你在代码里配置的 Base URL、API Key、Model ID 三件套和调用任何官方 SDK 的写法是一致的只是把 endpoint 指向了统一入口。这样做的价值在于你的 Agent 代码不需要为不同模型写多套适配层切换模型只改一个 Model ID 字符串。具体到 Claude Code 这类 Agent 的复现你需要准备三样东西第一是API Key在控制台的 API Keys 页面创建地址是 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite 。创建后立刻复制保存页面刷新后就看不到了。第二是Base URL统一用 https://taotoken.net/api 注意这个地址不带任何查询参数直接作为 SDK 的 baseURL 使用。第三是Model ID根据你的场景选。做 Agent 循环调试建议选支持工具调用tool use能力强的模型因为 ReAct 循环依赖模型返回结构化的 tool_use 块。你可以在模型对话页面先验证模型是否正常响应地址是 https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewrite 。如果你打算长期做 Agent 开发、频繁跑长对话和后台任务可以考虑Coding Plan地址是 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite 它更适合高频编码和 Agent 场景的持续调用。注意所有配置里的 Key 都不要硬编码进源码提交到 git。用.env文件 .gitignore是最基本的习惯工业级项目里这是红线。3. 可复制的项目结构配置TypeScript React(Ink) 分层骨架理解了前置准备接下来是重头戏怎么用 TypeScript 搭出一个工业级 Agent 的分层骨架。Claude Code 的源码结构给了很好的参考我把它抽象成一套你可以直接复制的目录布局。先看整体分层思路。工业级 Agent 大致分五层入口层main→ 会话层QueryEngine→ 执行层query 循环→ 工具层tools→ 服务层services。UI 层用 React(Ink) 独立挂在会话层之上通过状态订阅和主循环通信而不是耦合在一起。下面是我整理的可复制项目结构你可以直接建目录my-coding-agent/ ├── src/ │ ├── main.tsx # 入口并行初始化 │ ├── screens/ │ │ └── REPL.tsx # 交互式主循环 UI │ ├── query/ │ │ ├── query.ts # 单轮执行引擎generator │ │ └── QueryEngine.ts # headless/SDK 会话管理 │ ├── tools/ │ │ ├── tools.ts # 工具注册表 │ │ ├── FileReadTool.ts │ │ ├── BashTool.ts │ │ └── StreamingToolExecutor.ts │ ├── services/ │ │ ├── mcp/client.ts # MCP 客户端 │ │ └── compact/ # 上下文压缩 │ ├── state/ │ │ └── store.ts # 极简 pub-sub store │ └── utils/ │ └── queryContext.ts # system prompt 组装 ├── .env ├── tsconfig.json └── package.json关键配置在tsconfig.json里工业级项目对类型严格度要求很高建议开启这些选项{ compilerOptions: { target: ES2022, module: ESNext, moduleResolution: bundler, strict: true, noUncheckedIndexedAccess: true, exactOptionalPropertyTypes: true, jsx: react-jsx, skipLibCheck: true, esModuleInterop: true, resolveJsonModule: true, baseUrl: ., paths: { /*: [src/*] } }, include: [src/**/*] }noUncheckedIndexedAccess和exactOptionalPropertyTypes这两个选项在 Agent 项目里特别重要因为工具调用的参数是动态的类型不严格很容易在运行时炸掉。再看状态管理。Claude Code 的全局状态核心只有 36 行用最朴素的 pub-sub 模式通过 React 的useSyncExternalStore接入 Ink 组件树。没有 Redux、没有 Zustand。这个设计非常值得抄// src/state/store.ts type Listener () void; export function createStoreT(initialState: T) { let state initialState; const listeners new SetListener(); return { getState: () state, setState: (updater: (prev: T) T) { state updater(state); listeners.forEach((l) l()); }, subscribe: (listener: Listener) { listeners.add(listener); return () listeners.delete(listener); }, }; }为什么这么简单的 store 就够用因为 Agent 的状态流是确定性的消息历史、工具执行状态、权限模式这些都是单向流动的。引入复杂状态库反而增加心智负担。然后是 Agent 核心循环的骨架。query()是一个 generator 函数这是整个执行引擎的心脏// src/query/query.ts export async function* query(params: { messages: Message[]; tools: Tool[]; signal: AbortSignal; }): AsyncGeneratorQueryEvent { let messages params.messages; while (true) { // 1. 调用 LLM流式 const stream await callModel({ messages, tools: params.tools }); // 2. 流式接收遇到 tool_use 立即入队执行 const executor new StreamingToolExecutor(); for await (const chunk of stream) { if (chunk.type tool_use) { executor.addTool(chunk, params.signal); } yield { type: stream_delta, chunk }; } // 3. 等待工具执行完成 const toolResults await executor.drain(); if (toolResults.length 0) break; // 4. 把工具结果拼回消息进入下一轮 messages [...messages, ...toolResults]; } }这段骨架对应了 Claude Code 里最核心的 ReAct 循环。注意第 2 步的StreamingToolExecutor——它不是等 LLM 完整输出后再执行工具而是边流式接收边执行这是工业级和 demo 级的分水岭。工具注册表用条件编译的思路组织每个工具自己声明是否并发安全// src/tools/tools.ts export interface Tool { name: string; schema: object; isConcurrencySafe: (input: unknown) boolean; execute: (input: unknown, ctx: ToolContext) PromiseToolResult; } export const BUILTIN_TOOLS: Tool[] [ FileReadTool, BashTool, FileEditTool, // ... ]; export function filterToolsByDenyRules( tools: Tool[], denied: Setstring ): Tool[] { return tools.filter((t) !denied.has(t.name)); }isConcurrencySafe这个设计很关键。它不是简单的只读 vs 写入二分而是根据具体输入参数判断。比如FileReadTool通常返回 true但BashTool要看命令内容——ls是安全的rm -rf不是。这样调度器才能智能地把连续的安全工具并行执行把危险工具串行化。4. 本地验证跑通一次完整的 Agent 请求链路配置搭好了现在验证它能不能真正跑起来。这一步的目标是发一条消息看到模型返回 tool_use工具执行结果回传模型给出最终回答。整条链路跑通说明你的 Agent 骨架是活的。先写一个最小的验证脚本用 TaoToken 作为模型入口// scripts/verify-agent.ts import { query } from ../src/query/query.js; import { BUILTIN_TOOLS } from ../src/tools/tools.js; const BASE_URL process.env.TAOTOKEN_BASE_URL!; // https://taotoken.net/api const API_KEY process.env.TAOTOKEN_API_KEY!; const MODEL_ID process.env.MODEL_ID!; // 例如 claude-sonnet-4-5 async function main() { const controller new AbortController(); const events query({ messages: [ { role: user, content: 读取 package.json 并告诉我项目名 }, ], tools: BUILTIN_TOOLS, signal: controller.signal, }); for await (const event of events) { if (event.type stream_delta) { process.stdout.write(event.chunk.text ?? ); } if (event.type tool_use) { console.log(\n[tool] ${event.name} 入参:, event.input); } if (event.type tool_result) { console.log([result] ${event.output.slice(0, 120)}...); } } } main().catch(console.error);.env文件这样配TAOTOKEN_BASE_URLhttps://taotoken.net/api TAOTOKEN_API_KEYsk-你的key MODEL_IDclaude-sonnet-4-5运行npx tsx scripts/verify-agent.ts期望的成功结果是这样的输出序列[tool] FileReadTool 入参: { path: package.json } [result] { name: my-coding-agent, version: 0.1.0 ... 项目名是 my-coding-agent。如果你看到[tool]后面跟着工具名、[result]后面跟着文件内容、最后模型用自然语言总结了答案说明整条 ReAct 链路是通的。接下来验证流式工具执行这个关键特性。故意发一个需要多步工具调用的请求npx tsx scripts/verify-agent.ts # 输入列出 src 目录下所有 .ts 文件然后读取第一个文件的前 10 行观察日志的时间戳如果FileReadTool和BashTool或 GlobTool的执行是交错出现而不是严格串行说明StreamingToolExecutor的并行调度生效了。这是工业级 Agent 和普通 demo 最明显的区别之一。再验证权限过滤。在filterToolsByDenyRules里把BashTool加进 deny 集合重新跑一次。你会发现模型根本不知道 BashTool 存在——它不会尝试调用被禁用的工具因为被 deny 的工具连 schema 都不会发送给模型。这个设计比运行时拦截优雅得多。最后验证上下文压缩的触发。把AUTOCOMPACT_BUFFER_TOKENS设成一个很小的值比如 2000然后连续发多轮长消息。当 token 数接近阈值时你应该能在日志里看到压缩事件被触发历史消息被摘要替换。这一步能帮你理解为什么工业级 Agent 需要那么多压缩策略——单靠一种摘要压缩长对话里信息丢失会很严重。提示验证阶段建议把日志级别调高把每次 LLM 请求的 payload 大小、工具调用次数、token 用量都打出来。这些数据是后续调优的依据。5. 常见报错排查401、local proxy failed、reading choices 逐个击破链路跑通不代表一帆风顺实际调试中你会撞上一堆报错。这一节把最常见的几个拎出来对照真实错误信息给排查路径。报错一401 UnauthorizedError: 401 {error:{message:Invalid API key,type:authentication_error}}这是最高频的报错九成是 Key 配置问题。排查顺序先确认.env里的TAOTOKEN_API_KEY有没有多余空格或换行再确认代码里读取环境变量的时机——如果你在模块顶层就读了process.env而.env是后加载的就会读到 undefined。用dotenv的话确保import dotenv/config在所有业务 import 之前。还有一种隐蔽情况Key 创建后没复制完整。去 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite 重新生成一个复制时注意别漏字符。报错二local proxy failed / connection refusedError: connect ECONNREFUSED 127.0.0.1:7890 fetch failed: local proxy failed这个报错说明你的运行环境里配了本地代理但代理服务没起来或者端口不对。检查HTTP_PROXY/HTTPS_PROXY/ALL_PROXY这几个环境变量如果指向了一个不存在的本地端口就会报这个错。把无关的代理环境变量清掉让请求直连 https://taotoken.net/api 即可。报错三reading choices of undefinedTypeError: Cannot read properties of undefined (reading choices)这个报错通常出现在你混用了不同厂商的响应格式。choices是 OpenAI 风格的字段而 Anthropic 风格返回的是content数组。如果你用 Anthropic SDK 去解析一个返回 OpenAI 格式的响应或者反过来就会读到 undefined。解决办法是统一响应解析层在callModel里根据实际返回结构做适配别在业务代码里直接摸response.choices或response.content。报错四OAuth token expired / invalid_grantError: OAuth token expired, please re-authenticate {error:invalid_grant}如果你在 Agent 里集成了 OAuth 流程比如某些 MCP 服务器的认证token 过期会报这个。工业级做法是在 token 快过期时自动刷新而不是等报错再处理。检查你的 token 刷新逻辑有没有正确设置expires_at判断以及 refresh token 有没有被覆盖丢失。报错五tool_use 参数解析失败Error: Unexpected token in JSON at position 42 Tool input validation failed for BashTool这是流式接收 tool_use 时的经典问题。模型返回的 tool_use 参数是分块流式传输的如果你在第一个 chunk 就尝试JSON.parse必然失败。正确做法是累积所有 input_json_delta 后再解析。检查你的StreamingToolExecutor.addTool有没有做完整的 JSON 拼接。排查这类问题的通用思路是先定位是配置层、网络层、还是解析层的问题。401 和 proxy failed 属于配置/网络层reading choices 和 JSON 解析属于解析层OAuth 属于认证层。分层定位能省掉大量瞎试的时间。6. 从架构到落地把 Agent 循环用起来拆完架构、跑通链路、排完错最后聊聊怎么把这套东西真正用起来。工业级 AI Coding Agent 的价值不在于能调模型而在于它把调度、权限、上下文、扩展这几件事做成了可复用的工程模式。如果你想继续深入我建议按这个顺序推进先把query()循环和StreamingToolExecutor吃透这是 Agent 的心脏然后研究权限系统的多阶段校验理解为什么工业级产品要在安全上花这么大功夫最后看多 Agent 协作和后台记忆管线这两块是拉开产品差距的地方。对于长期做 Agent 开发的场景稳定的模型调用入口是基础。你可以从模型对话页面 https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewrite 先验证模型能力再参考接入文档 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 把配置固化到项目里。如果要做持续的编码 Agent 或复杂工作流Coding Plan https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite 会更合适。最后分享一个我踩过的坑别一上来就追求功能齐全。Claude Code 有 60 工具、多套压缩策略、复杂的权限模型但这些都是长期迭代出来的。你先跑通一个工具 一次循环 一次结果回传再逐步加并行、加权限、加压缩。架构的复杂度应该由真实需求驱动而不是照着别人的成品堆砌。把最小闭环跑稳比什么都重要。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进