这个话题最近在圈子里吵得挺凶尤其是“Rust 输了”“TypeScript 才是唯一的神”这种说法看着就像钓鱼标题但它确实点出了一个正在发生的分化AI Agent 的应用层开发TypeScript 的占比正在肉眼可见地增长而 Rust 则被更多人定位到引擎、运行时、高性能推理这些底层层面上。我自己两边都写过既用 Rust 折腾过 Agent 跑批也用 TypeScript 快速搭过面向业务的 Agent 服务说实话两边各有各的难受和舒服。这篇文章不站队只把我在实操里看到的真实差异、踩过的坑和一些能直接拿走的方案写出来给正在做技术选型或者准备入坑 AI Agent 开发的朋友做个参考。1. 先别急着吵“谁赢了”看清 AI Agent 开发的分层结构1.1 Agent 开发从来不是“一门语言”的问题很多人争论 Rust 和 TypeScript 哪个更适合 Agent其实是把“Agent”当成一个单一的东西来比。但真实的 Agent 项目拆开看至少有三层职责完全不同第一层是模型接入与会话编排层要处理 prompt、上下文、工具调用、状态流转第二层是 Agent 业务逻辑层也就是工具函数、外部 API 对接、数据加工和权限控制第三层是高性能计算与执行引擎层比如向量检索、规则引擎、并发调度、类人任务复盘这类重计算逻辑。这里最容易被忽略的事实是大多数业务团队写 Agent90% 的时间都耗在第一层和第二层。这一层的核心复杂点根本不在“每秒能执行多少万亿次浮点运算”而在“怎么把一套不稳定的 LLM 输出稳定地编排成可用的业务流程”。TypeScript 恰好在这两层拥有极高的表达效率和生态覆盖。而 Rust 的优势更多体现在第三层比如底层的推理服务、Embedding 引擎、网关代理这类基础设施。如果你去做一个面向终端用户的客服 Agent瓶颈往往是 prompt 调优、工具返回格式校验、上下文窗口管理而不是 CPU 跑不满。此时选 Rust 不会让你在业务上取得优势反而会因为迭代速度变慢而拖累整个 Agent 的效果优化周期。1.2 语言本身只是“生态位”竞争Rust 在 Agent 场景的瓶颈很真实我最早也用 Rust 写过 Agent 原型那套代码至今还在我的 GitHub 私有仓库里吃灰。坦白说Rust 给我的体验是写底层逻辑很爽但写业务型 Agent 很痛苦。LLM SDK 生态差距明显。OpenAI、Anthropic、Google 等主流模型厂商的官方 SDK 基本都是 Python 和 TypeScript 优先Rust 社区虽然也有async-openai这类不错的库但更新节奏和官方接口的同步速度始终有滞后尤其当模型厂商开始频繁调整工具调用格式、流式事件结构时Rust 的适配往往慢半拍。Agent 模式需要高频调整。你的 tool prompt 可能要改几十个版本消息处理逻辑也可能因为模型行为变化而调整。Rust 的强类型和借用检查在这种“高频试错期”反而成了负担编译时间会打断你调 prompt 的思路。异步并发虽然强但 Agent 场景的并发没那么“硬核”。Rust 的 async 生态很强但大多数 Agent 服务的并发瓶颈在 LLM API 的限流上不在语言本身的并发能力上。用 Node.js 的事件循环已经完全够撑住常见的 Agent 并发请求量没必要为一个普通业务付出 Rust 的开发成本。所以我的观点很直接Rust 没有输在“技术上”它只是不太适合 Agent 开发的核心痛点——快速试错、动态协议、生态对齐。TypeScript 之所以在这个战场越来越主流根本不是因为“性能更强”而是因为它让 Agent 的“工程化迭代”成本降到了最低。2. TypeScript 能成为 Agent 层默认选择的几个底层原因2.1 Agent 系统的本质是“协议处理”不是“计算密集”理解这一点才能理解为什么 TypeScript 会在 Agent 层胜出。一个 Agent 的工作循环大致是把用户输入塞进消息列表调用模型模型返回文本或者工具调用请求Agent 根据工具调用去外部执行动作再把结果返回给模型继续下一轮。整个过程的核心是数据格式转换与协议对齐用户消息变成 API 请求API 响应变成内部消息结构工具参数要做 JSON 解析和校验工具结果又要序列化回上下文。这一连串操作的共同关键词是“结构化数据”。TypeScript 的类型系统在处理这种“中间层数据转换”时有着天然优势。你可以用 interface 精确描述 Tool 结构、Message 结构、ToolCall 结构并且通过类型推导让整个调用链路的参数都在编译期被检查。这不是玄学是我在实际项目里能明显感受到的差异Rust 也能做序列化但你需要引入 serde、设计错误类型、处理一堆生命周期问题而在 TypeScript 里一个干净的 interface 加一个泛型工具类型就能解决问题。再加上 Agent 的系统边界天然适合事件驱动模型。Node.js 的 EventEmitter、流式处理能力、WebSocket 支持让 Agent 天生适合做“流式接收模型回复、边接收边推送前端”的架构。用 Rust 实现同样的流式体验虽然技术上是可行的但代码复杂度会明显上一个台阶。2.2 类型系统LLM 输出是不可信的TS 帮你把边界看得清清楚楚Agent 开发里最坑爹的一点是模型的输出永远不可信。你说“请以 JSON 格式返回”它可能给你夹带一段 Markdown可能把 key 从city写成City甚至可能凭空多出一个字段。这时候类型系统就是你最好的安全网。我比较推荐用zod这类运行时校验库来给模型输出做校验。不要只依赖 TypeScript 的静态类型因为静态类型在编译后会被擦除运行时完全不存在。你需要一套“既是类型定义又能动态校验”的方案让程序在收到模型输出时立刻判断“这玩意儿能不能用”。import { z } from zod; // 定义一个工具参数的结构 const WeatherSchema z.object({ city: z.string().describe(城市名如 北京), unit: z.enum([celsius, fahrenheit]).optional().describe(温度单位默认 celsius), }); // 从 Schema 反推静态类型全程只需维护一份定义 type WeatherInput z.infertypeof WeatherSchema; // 用 safeParse 处理模型传进来的脏数据 function parseWeatherInput(raw: string): WeatherInput | null { try { const parsed WeatherSchema.safeParse(JSON.parse(raw)); return parsed.success ? parsed.data : null; } catch { return null; } }这段代码解决了两层隐患。第一层是模型可能返回非法 JSONJSON.parse直接抛异常这里通过try...catch兜住并返回 null第二层是模型返回了合法 JSON 但字段结构不对比如忘了传city或把unit拼错safeParse会拦住它。相比“把模型输出直接as成目标类型”的写法这种带运行时校验的方式在 Agent 上线后的稳定性表现完全不同。2.3 工具调用是 Agent 的命根子TypeScript 的函数式表达恰好匹配实际设计 Agent 时每个工具本质上就是一个“外部动作的描述 动作实现”。TypeScript 可以把这两部分塞进同一个模块维护起来非常顺手。interface Tool { name: string; description: string; parameters: z.ZodObjectany; execute: (args: any) Promisestring; } const weatherTool: Tool { name: get_weather, description: 查询指定城市当前的天气状况。当用户询问天气时使用。, parameters: z.object({ city: z.string().describe(城市中文名), }), async execute({ city }) { // 这里通常是调用第三方天气 API可以做成 async const data await fetchWeather(city); return JSON.stringify(data); }, }; const tools [weatherTool];当你把工具定义成这种格式后生成给模型的功能描述就很自然了把name、description、parameters里的 JSON Schema 抽出来拼成一个数组塞进 API 请求即可。而且扩展新工具的时候完全不用改 Agent 主循环只需要增加一个Tool对象。这也是我反复强调的观点Agent 工程的迭代主要发生在“工具层”而 TypeScript 对“函数对象 元数据”的组合支持让这一层的开发体验非常自然。相比之下Rust 需要定义每个工具的参数结构体、错误类型和异步执行函数还要额外处理 trait object 的生命周期心智负担确实更重。2.4 Node 的事件循环让流式 Agent 体验“零成本”实现做 Agent 面向终端用户的产品时流式输出基本是刚需。用户不想盯着菊花转三秒而是希望看到文字一个字一个字蹦出来。TypeScript/Node.js 天然是流式友好的fetch返回的ReadableStream、for await...of语法、WebSocket 推送、SSE 标准都是 Node 生态的一等公民。Rust 当然也能做流式但异步流 (Streamtrait) 的抽象层会让前后端对接麻烦很多。尤其是当你还需要把流式事件转发给前端时Rust 需要额外处理 web_socket / SSE 的复杂细节工程速度明显下降。这里并非说 Rust 做不到而是说“用 Rust 做同样的东西所花费的时间可能是 TypeScript 的两倍以上”在创业团队或 To B 交付场景中这个时间成本往往不可接受。3. 实操拆解用 TypeScript 从零搭一个可用的 Agent3.1 初始化项目与依赖选型我建议用pnpm和tsx启动项目理由很简单tsx让 TypeScript 可以直接运行不用先配置繁琐的ts-node路径别名。mkdir my-agent cd my-agent pnpm init pnpm add typescript tsx zod openai这里我用openai包举例因为它不只支持 OpenAI 官方模型很多兼容 OpenAI 协议的中转服务和本地模型框架也能用。如果你接的是其他家可以找到对应的 SDK核心逻辑是一样的。初始化tsconfig.json要特别注意strict必须开启。Agent 代码里大量数据来自运行时如果类型不够严格遗漏的错误会悄悄溜到线上。{ compilerOptions: { target: ES2022, module: ESNext, moduleResolution: Bundler, strict: true, esModuleInterop: true, skipLibCheck: true } }3.2 定义消息结构与模型调用封装Agent 核心就是一个会话循环。为了不让代码散得到处都是我习惯先把“消息”和“模型”两个抽象做扎实。export interface ChatMessage { role: user | assistant | tool; content: string | null; tool_calls?: ToolCall[]; tool_call_id?: string; } export interface ToolCall { id: string; function: { name: string; arguments: string; }; }模型调用封装的关键在于处理tool_calls字段。不同模型对工具调用的返回格式存在细节差别比如有的模型在函数参数里把 JSON 序列化成字符串有的直接返回 JSON 对象。我选择统一用字符串形式接收再在自己这边二次解析因为字符串在不同 SDK 版本间兼容性最好。import OpenAI from openai; const client new OpenAI({ apiKey: process.env.OPENAI_API_KEY, }); export async function chatOnce(messages: ChatMessage[], tools: any[]) { const response await client.chat.completions.create({ model: gpt-4o-mini, messages: messages as any, tools: tools.length 0 ? tools : undefined, temperature: 0.2, }); const msg response.choices[0].message; return { role: assistant, content: msg.content, tool_calls: msg.tool_calls?.map((tc) ({ id: tc.id!, function: { name: tc.function.name!, arguments: tc.function.arguments!, }, })), } satisfies ChatMessage; }这里temperature: 0.2是我推荐 Agent 执行工具调用时的默认值。温度太高会导致模型频繁“发挥创造力”编造工具参数太低又可能导致文本生成部分过于死板。0.2 左右是一个平衡点既让工具调用稳定又不至于让自然语言回复像机器人一样僵硬。3.3 主循环给 Agent 装上“有限步数”的刹车Agent 主循环有个经典风险模型反复要求调用工具形成死循环。不加步数限制的话一个 bug 就能让你的 token 账单瞬间爆炸。我一般给循环设置硬上限比如 8 轮超过就强制中断并返回当前结果。export async function runAgent(userInput: string) { const messages: ChatMessage[] [ { role: user, content: userInput }, ]; const maxSteps 8; for (let step 0; step maxSteps; step) { const reply await chatOnce(messages, toolsMeta); messages.push(reply); // 没有工具调用说明 Agent 认为可以直接回复用户了 if (!reply.tool_calls || reply.tool_calls.length 0) { return reply.content; } // 依次执行所有工具调用 for (const call of reply.tool_calls) { const result await dispatchTool(call.function.name, call.function.arguments); messages.push({ role: tool, tool_call_id: call.id, content: result, }); } } return Agent 已达到最大执行步数请尝试把问题拆得更细。; }这里有个容易踩的细节把工具执行结果塞回messages时要用tool_call_id对上模型返回的call.id而不是随便挂一个字符串。部分模型对tool_call_id的对应关系非常敏感对不上会直接报错或忽略工具结果。3.4 工具分发器错误处理比执行本身更重要工具执行的目标是把结果顺利交给模型而不是真的“执行成功”。所以我把 dispatch 函数的返回值设计成“永远是字符串”哪怕是执行失败也要把错误信息拼成字符串返回给模型让模型自己判断如何处理。async function dispatchTool(name: string, argsJson: string): Promisestring { const tool toolMap[name]; if (!tool) { return 错误未找到工具 ${name}; } const parsed tool.parameters.safeParse(JSON.parse(argsJson)); if (!parsed.success) { // 关键把校验错误原样返回模型能自己修正参数 return 参数校验失败: ${parsed.error.message}; } try { const output await tool.execute(parsed.data); return output; } catch (err: any) { return 工具执行异常: ${err.message}; } }很多新手在工具执行失败时直接抛异常导致整个 Agent 崩溃。但实际上模型具备很强的“自我修正能力”只要你把失败原因描述清楚模型会自己调整参数重新调用。比如天气工具需要城市编码模型第一次可能传中文名你返回“城市编码查询失败需要城市名拼音”第二轮它大概率就改过来了。这个“错误即提示”的设计是 Agent 工程里最实用的小技巧之一。3.5 接入流式输出与中间状态推送如果想让 Agent 在等待模型回答时不让用户干等可以在主循环里埋入“进度事件”。我常用的是 EventEmitter 或者直接通过 WebSocket 把当前步骤推送出去。import { EventEmitter } from events; export function createAgentRunner() { const emitter new EventEmitter(); async function run(userInput: string) { const messages [{ role: user, content: userInput }]; for (let step 0; step 8; step) { emitter.emit(agent:thinking, { step }); // 调用模型... // 执行工具... emitter.emit(agent:tool, { name: call.function.name, args: call.function.arguments, result, }); } } return { run, on: emitter.on.bind(emitter) }; }前端就能实时看到“Agent 正在查询天气”“Agent 正在计算方案”这类过程状态。这听起来简单但对 Agent 类产品的体验提升是决定性的因为模型调用耗时常常达到几秒甚至十几秒如果没有中间反馈用户早就关掉页面了。4. Rust 不是输了而是换了个更合适的位置4.1 Rust 真正能打的 Agent 场景如果把话说得更严谨一点Rust 在 AI Agent 领域不但没有“输”甚至在一些局部战场是无可替代的。高性能日志与监控采集Agent 在运行过程中会产生大量事件日志如果要求低延迟、高吞吐的数据采集管道Rust/Go 这类编译型语言优势明显。TypeScript 的 GC 停顿和单线程模型在高吞吐日志采集下会比较吃力。嵌入式 Agent 边缘节点在小型设备上跑 Agent比如智能音箱、边缘网关Rust 的内存占用和执行效率远优于 Node.js 运行时。特别是设备内存只有几十 MB 的场景Node 根本装不进去。安全敏感的工具沙箱层Agent 的工具执行往往涉及代码执行、命令调用、权限判断。如果要在沙箱里执行用户逻辑Rust 的 WASM 生态提供的内存安全能力比 JS 的 eval 系列方案可靠得多。底层推理网关与流量治理Agent 服务需要把请求路由到不同模型、做限流、做 fallback这个网关层用 Rust 写可以压到极低的延迟和开销。所以说 Rust 的真正位置在“Agent 的基础设施层”和“高性能/嵌入式组件层”。Rust 不是输了它只是和 TypeScript 服务了不同的需求。4.2 混合架构TypeScript 编排 Rust 引擎经过大量项目实践后现在我最推荐的架构是混合模式外层用 TypeScript 写业务的 Agent 编排、会话管理、工具接入把重计算或者安全敏感组件用 Rust 写成独立服务通过 gRPC 或 HTTP 暴露给 Node.js 调用。TS Agent 编排层 ↓ (gRPC) Rust 服务向量检索 / 代码沙箱 / 数据采集这样的好处是日常迭代集中在 TypeScript 编排层改动成本低能快速响应业务需求而 Rust 放在稳定、偏计算的底座上一旦写好就不需要频繁改动弥补了 Rust 迭代慢的短板。我在一个实际项目里把语义检索模块用 Rust 实现后通过 napi-rs 做成 Node 原生模块直接调用运行时性能提升了 8 倍而且不用处理子进程管理问题。如果你还没上过 napi-rs这里也简单提一句它是当前 TS/Rust 混合开发最成熟的方案。在 Rust 侧实现一个函数后用 napi 导出Node 侧就像 require 一个普通模块一样使用。相比用命令行调用 Rust 二进制再去解析 stdoutnapi 方案在开发体验和运行性能上都要好一个量级。4.3 给 2026 年的技术选型一个可执行的判断框架很多正在学习的同学会纠结。我给一个实用框架不需要懂太多底层原理只需要回答三个问题第一个问题你的 Agent 是给用户用的应用还是给其他系统用的核心库如果是前者直接选 TypeScript 起步。后者则看核心库的性能敏感度如果每秒需要处理上万次请求优先考虑 Rust。第二个问题产品的迭代重心是业务逻辑的快速扩展还是底层执行效率的极致提升业务逻辑复杂且天天改TypeScript 会是你的好朋友。如果重点是模型推理效率和并发引擎Rust 更合适。第三个问题你的团队当前的技能栈是什么Node 技术栈可以平稳迁移到 TS Agent 开发而 Rust 团队的成员需要时间去适应快速试错的节奏。很多项目失败不是语言不够好而是团队和语言节奏不匹配。按照这个框架大部分做应用型 Agent 的团队会得到“TS 优先Rust 做底座”的结论。这个结论不是标题党的“唯一的神”而是现实项目里被反复验证过的工程判断。5. 真实项目里的踩坑记录与排查技巧5.1 类型定义和运行时数据处理脱节这是我见到的最高频问题。很多同学习惯把模型返回的数据直接as SomeType结果运行时模型某次抽风返回了不一样的结构整个代码就炸了。正确做法是像前文那样用 zod 做运行时校验或者退一步用自定义类型守卫函数function isToolCall(obj: any): obj is ToolCall { return ( obj typeof obj.id string obj.function typeof obj.function.name string typeof obj.function.arguments string ); }类型守卫的好处是不引入额外依赖也能给后面的代码提供类型收窄。但字段一多还是 zod 更省事。我的习惯是简单结构用类型守卫复杂结构直接上 zod。关键原则就是“绝不让未经验证的数据流进核心逻辑”。5.2 token 燃烧上下文爆炸是 Agent 最大隐性成本Agent 每多执行一轮工具调用就要把前面所有消息重新发给模型。这意味着上下文长度呈“滚雪球”式增长10 轮工具调用之后你发送的内容可能已经翻了四五倍。账单上的数字增长会非常明显。几个我实际用过的缓解手段消息精简。工具执行结果字段太长时只保留核心片段再不行做个摘要再塞回上下文。会话滑动窗口。超过一定轮数后把早期消息压缩成一段系统级摘要。虽然摘要会丢失细节但总比 token 爆掉强。工具结果“截断上报”。比如查询用户订单时如果返回 100 条数据只给模型传前 20 条并在文案里标明“仅展示前 20 条如需更多请指定筛选条件”。5.3 模型不按 JSON Schema 调用工具有时候模型给出的工具参数和我们的 zod Schema 对不上比如要求unit必须是字符串但它传了一个数组。不要慌这种情况的处理方式是在 dispatch 失败时把详细的校验错误返回给模型参数校验失败: Expected string, received array at unit模型在下一轮看到这个具体报错后大概率会自我修正。这比你在代码里直接 throw 一个异常要友好得多。另外如果模型频繁输出非 JSON 内容检查一下你在 temperature 上是否设得过高。高于 0.7 后模型“幻觉”概率会明显上升。Agent 场景的默认值 0.2 也适用于大多数工具调用任务。5.4 安全问题不要信任模型的任意工具调用能够调用外部工具的 Agent 本质上拥有了“行动能力”。如果你让它能读文件、发邮件、操作数据库一旦 prompt 被注入后果会很严重。必要的防御手段包括所有敏感操作必须在代码里二次确认不要直接交给模型一条龙执行。比如发邮件强制要求用户点击“确认发送”按钮才能触发。每个 Agent 分配最小权限账号禁止使用管理员权限跑工具。外部输入包括网页抓取内容、邮件正文要标记为不可信数据不能让用户内容直接覆盖系统指令。在系统 prompt 里用“输入分隔符”把外部内容和指令区分开降低 prompt injection 的风险。5.5 可观测性从第一天就给 Agent 写日志Agent 的“黑盒感”比普通后端服务更强因为它的行为模式是不确定的。同一句话在相同配置下可能走不同的工具路径。没有日志你根本无法排查“为什么某个问题到了 Agent 那里突然答非所问”。我的做法是在每条消息和每次模型调用前后都打结构化日志格式类似[step2] user: 北京天气怎么样 [step2] model: 调用 get_weather(city北京) [step2] tool_exec_start: get_weather [step2] tool_exec_done: get_weather 耗时 320ms日志不需要很花哨但必须能还原当时 Agent 的完整决策链路。在此基础上我会再加一层 token 用量统计为后续调优提供数据依据。很多 Agent 质量问题光靠看代码很难复现但日志拉出来问题往往十秒钟就能定位。5.6 常见问题速查表现象可能原因快速解法模型不返回工具调用工具描述不够清晰或温度过高降低 temperature 到 0.2 以下重写工具描述加入使用示例工具参数解析失败模型返回了非 JSON 格式在 dispatch 中捕获解析错误并回传给模型让它自修正Agent 死循环没有步数上限或工具结果不改变决策增加 maxSteps 硬上限并检查工具结果是否被正确塞进上下文回复越来越慢messages 里上下文太长做消息摘要、截断工具结果账单飙升每一轮都携带完整历史消息加 token 统计滑动窗口压缩历史Agent 执行了不该执行的操作prompt 注入或权限过大高风险操作触发人工二次确认使用最小权限原则最后分享一点个人的工程体会标题那种“唯一的神”的说法我还是觉得太绝对了。技术选型永远不是找一个银弹而是在正确的位置用正确的工具。我在实际项目里的体会是把 AI Agent 的应用层交给 TypeScript可以换来极高的迭代效率和清晰的数据流边界这对绝大多数业务团队来说是最宝贵的价值而 Rust 则会继续在底层引擎、边缘计算、高性能基础设施这些不显山不露水的地方发光发热。它俩不是取代关系而是像前后端分工一样各自守住自己的战场 如果你正准备上手 Agent 开发我建议别被语言之争带偏。先拿 TypeScript 把最小可用 Agent 跑通去理解工具调用循环、消息管理和错误恢复这些真正核心的问题当你遇到性能天花板时再把热点模块用 Rust 替换也不迟。到那时候你会发现真正的“神”不是某种语言而是你对 Agent 系统本身的理解深度。