我创有一个 AI 交流群,如果你想学习 Agent 项目落地或者想交流 Agent 技术的的可以加我v
yunmz777进行了解,
一提到 AI 开发,我们很容易先想到 Python,但看 Claude Code、Gemini CLI、OpenCode、Pi 这些 Coding Agent,又会发现 TypeScript 出现得特别频繁。
这里说的 Harness,可以理解为模型外面负责实际运行任务的那套程序,它管理会话、组织上下文、调用工具,也要处理用户中断、执行结果和任务恢复。我们在终端或编辑器里看到的 Agent,背后就需要这些能力。
我觉得,讨论这个现象时,应该先把开发者每天要做的事情摆出来。模型通过 API 提供能力以后,围绕它构建产品,会遇到大量我们熟悉的应用开发问题,TS 恰好能把其中不少事情接在一起。
Harness 也有其他实现路线,比如 Codex 已经转向 Rust,所以我更想聊的是:为什么这么多团队会选择 TS,以及什么时候这份便利值得保留,什么时候又需要换一种做法。
Harness 每天在忙什么
假设我们让 Agent 帮忙修改一个项目,它需要先找到相关文件,把必要的内容交给模型,再根据模型返回的工具调用检查参数、执行操作,并把结果送回去。过程中,界面要持续显示进度,用户也可能修改要求或随时按下停止。
真正动手以后,还会碰到更多细节:命令执行到一半失败了,文件已经改了但会话还没保存,用户关闭窗口后又重新打开,某次工具调用一直没有返回。
这些都是 Harness 要处理的问题,它的核心循环可以很短,但围绕循环建立的执行和状态管理不能省。
对于主要调用远程模型的产品,很多时间花在等待网络、文件、数据库和工具进程上,Node.js 的事件循环和非阻塞 I/O正好适合组织这类工作。TS 提供类型检查,实际执行异步操作的是 Node.js、Bun 这些运行时。
Python 的机器学习生态很强,但模型由远程服务提供以后,我们未必需要在 Harness 里运行训练和推理相关的库,更经常需要处理的是请求、事件、工具和界面。选型时,这些工作占了多大比重,自然就很重要。
Reddit 的一篇相关讨论里,就有开发者提到:很多 Agent 工作其实是调用 API,产品开发者会沿用自己熟悉的技术栈。
异步能力并非 TS 独有,Python 的 asyncio 同样适合并发 I/O,不能把 GIL 理解成 Python 无法同时等待多个网络请求。这里值得比较的是开发习惯和配套生态。
一个功能经常要从运行时一直改到界面
做过带工具调用的聊天界面,就会发现,很多功能没办法只改一个地方。
比如我们要加一个文件修改审批,工具执行层要产生审批请求,界面要展示修改内容,用户确认后运行时才能继续。时间线还要记录这次操作,会话恢复时也得知道它有没有完成。
一个看起来很小的功能,可能涉及工具执行、消息协议、状态存储和 UI。
如果这些代码都在 TS 工程里,我们可以共享事件类型和运行时校验规则,某个字段改了,相关调用处可以一起检查,前端展示的状态也更容易和运行时保持一致。
这对已经使用 React、Node.js 的团队很有吸引力,因为我们不必为了把两种语言接起来,额外维护两份接口定义,或者再建立一套代码生成流程。
产品运行的环境也会影响选择:VS Code 的本地和远程扩展宿主使用 Node.js,Electron 主进程也有 Node.js 环境,界面则沿用 Web 技术,在这样的基础上继续写 TS,通常很顺手。
终端产品也能复用这套经验,比如 Gemini CLI 的官方工程说明就列出了 TypeScript、React 和 Ink,并把终端界面与模型编排拆在不同的包里。
我认为,同一功能涉及的几层代码可以一起修改,是 TS 很实际的优势,语言统一能减少协调成本,模块和进程边界则仍然需要认真设计。
类型和工具参数可以一起维护
Harness 里有很多看起来相似、实际又不能混用的数据,比如文本增量和工具结果需要不同的字段,等待审批和执行失败对应不同的处理方式,读取文件与修改文件的工具也有各自的参数要求。
TS 的联合类型、泛型和类型推导适合描述这些关系,不过对 Agent 来说,只有静态类型还不够。
模型返回的数据来自系统外部,我们在代码里写了一个接口,并不代表模型实际返回的 JSON 就符合它,所以 TS 经常需要配合 Zod、TypeBox 或其他运行时校验方案。
例如,一个读取文件的参数定义可以写成:
import { z } from "zod";
const ReadFileInputSchema = z.object({
path: z.string(),
startLine: z.number().int().positive().optional(),
});
type ReadFileInput = z.infer<typeof ReadFileInputSchema>;
// 模型返回的参数要经过运行时校验
const input = ReadFileInputSchema.parse(modelOutput);
这样,我们既能从 Schema 推导代码里的类型,也能检查运行时传进来的参数,在支持这类 Schema 的 SDK 里,还可以用它向模型描述工具,AI SDK 的 Tool Calling 文档就展示了这套配合方式。
它的好处很具体:给工具增加一个参数时,我们更容易把模型能看到的描述、执行函数接收的数据和相关调用一起维护。
需要注意的是,写一个 as ReadFileInput 不会验证数据,TypeScript 的类型在运行时会被移除,外部输入仍要实际检查。
参数通过校验,也不等于操作一定合理,比如路径是字符串,不代表它就在允许访问的目录里,工具的权限、执行条件和结果检查还是要由程序负责。
所以我看重的是类型与运行时契约配合起来的便利,它能减少接口不一致,却不能替我们保证 Agent 的每一步都正确。
现成的库和插件会影响最终选择
语言选型还有一部分很朴素:我们缺的东西,需要自己补多少。
在 Reddit 的另一篇讨论里,一位长期使用 Kotlin 的开发者解释,他最后选 TS,是因为适用环境广、现成库也足够,如果坚持原来的语言,就得自己补不少东西。
做 Harness 时,我们要接模型 SDK、处理流式输出、管理子进程,也可能要接浏览器、编辑器和各种外部服务,对于熟悉 TS 的团队,能从已有工程里拿来多少东西,会直接影响开发成本。
插件也是一样,有人想在工具调用前检查参数,有人想增加自定义工具,如果扩展方式和他们已有的开发习惯接近,就更容易开始动手。
OpenCode 的插件机制支持本地 JS、TS 文件,也支持 npm 包,插件函数拿到上下文,返回需要注册的 hooks,就能接入相应事件。
这样的机制也会带来积累,现成扩展越多,后来的人越容易沿着同一套技术继续开发。
其他语言并没有被协议挡在外面,MCP 官方 SDK 列表里,Python、Go、Rust 等也有正式支持。TS 的吸引力,更多在于已有工具能不能顺利放进我们的工程。
Pi 团队在 Pi Durable 的介绍里,直接回答过为什么继续选 TypeScript:
Because it is the easiest way to bootstrap this.
他们的解释很直接:用 TS 把这套系统搭起来最省力,这也很能说明实际选型时的优先级。
Claude Code 还考虑了模型熟悉什么
做开发工具时,我们通常会考虑团队熟悉什么,Claude Code 团队还多考虑了一件事:参与开发的模型熟悉什么。
在 2025 年 9 月的团队采访里,Boris Cherny 解释,他们希望使用 Claude 已经熟悉的技术栈,让 Claude Code 能直接参与开发自身,而 TypeScript 和 React 正是当时模型比较擅长的技术。
当我们经常让 Coding Agent 修改项目时,技术栈是否常见、错误反馈是否明确、修改后是否容易验证,都会影响实际体验,而类型检查就能提供一部分明确反馈,让模型继续修正接口或参数错误。
不过,这只是 Claude Code 团队当时的选择依据,不能因此推导出所有模型、所有项目都最适合 TS。
对我们来说,更有用的问题是:手上的模型能不能在这套工程里稳定完成修改,我们又能不能及时发现它改错了什么。
Codex 转向 Rust 也有充分的理由
看到这里,很容易产生另一个疑问:既然 TS 这么方便,Codex 为什么又改用 Rust?
Codex 在 2025 年 5 月的官方迁移公告里,把理由讲得很直接:
- 减少安装依赖,避免要求用户预装特定版本的 Node.js
- 更直接地使用原生安全与沙箱绑定
- 降低运行时和内存开销
- 通过协议继续支持其他语言扩展
同一篇公告也肯定了 React 和 Ink,认为早期终端界面很容易搭建和迭代。
这两件事并不矛盾,开发阶段需要快速做出交互、验证功能,而当软件要交付给更多用户时,安装、常驻内存和系统能力也会变得更加突出。
GitHub 后来公开的 Copilot Agent Runtime 迁移也有类似考虑,核心从 TypeScript、Node.js、V8 迁到 Rust 后,团队更方便控制资源开销,并支持其他语言的应用选择进程内嵌入。这里迁移的是核心运行时,界面和各语言 SDK 属于另外的层次。
即便模型响应需要等待,本地软件的成本仍然值得优化,因为一个会话可以接受的内存占用,在同时运行很多会话时,也可能成为问题。
TS 软件也能做成独立程序,Bun 的可执行文件打包会把代码和运行时一起交付,用户不必先安装对应环境。不过,普通打包仍然包含 JS 运行时,改善分发方式并不意味着所有运行开销都会消失。
我会分别看安装、启动、内存、并发和系统接口,而不会只看项目最后写成了哪门语言。
如果是我们来做
如果产品已经使用 Electron、React、Node.js,我会先用 TS 把 Harness 的主体做好,因为会话、模型适配、工具协议、事件流和界面经常需要一起修改,沿用现有技术栈,能让我们把更多时间放在这些行为是否正确上。
同时,我会把耗时计算和外部工具的执行边界拆清楚,为大文件解析、复杂索引、长时间运行的命令安排合适的执行位置。给函数加上 async 不会自动把 CPU 计算移到其他线程,Node 官方也明确提醒要避免让耗时计算阻塞事件循环。
是否换语言,还是要看现在最影响产品的是什么:如果问题在工具参数、上下文组织、任务取消或会话恢复上,就先把这些地方做好,如果已经明确卡在启动、内存、系统接口或跨语言嵌入上,再考虑原生实现。
对我们而言,选择 TypeScript 的主要理由,是能用熟悉的一套技术把交互、工具和 Agent 流程做完整,再根据实际需要补上底层能力。