这个话题最近在圈子里吵得挺凶,尤其是“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 对接、数据加工和权限控制;第三层是高性能计算与执行引擎层,比如向量检索、规则引擎、并发调度、类人任务复盘这类重计算逻辑。
这里最容易被忽略的事实是:大多数业务团队写 Agent,90% 的时间都耗在第一层和第二层。这一层的核心复杂点根本不在“每秒能执行多少万亿次浮点运算”,而在“怎么把一套不稳定的 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.infer<typeof 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; } }这段代码解决了两层隐患。第一层是模型可能返回非法 JSON,JSON.parse直接抛异常,这里通过try...catch兜住并返回 null;第二层是模型返回了合法 JSON 但字段结构不对,比如忘了传city或把unit拼错,safeParse会拦住它。相比“把模型输出直接as成目标类型”的写法,这种带运行时校验的方式在 Agent 上线后的稳定性表现完全不同。
2.3 工具调用是 Agent 的命根子,TypeScript 的函数式表达恰好匹配
实际设计 Agent 时,每个工具本质上就是一个“外部动作的描述 + 动作实现”。TypeScript 可以把这两部分塞进同一个模块,维护起来非常顺手。
interface Tool { name: string; description: string; parameters: z.ZodObject<any>; execute: (args: any) => Promise<string>; } 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 从零搭一个可用的 Agent
3.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): Promise<string> { 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 二进制再去解析 stdout,napi 方案在开发体验和运行性能上都要好一个量级。
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 那里突然答非所问”。
我的做法是在每条消息和每次模型调用前后都打结构化日志,格式类似:
[step=2] user: 北京天气怎么样 [step=2] model: 调用 get_weather(city="北京") [step=2] tool_exec_start: get_weather [step=2] 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 系统本身的理解深度。