Typesafe编码智能体实战:用类型检查闭环提升AI代码质量
1. 从一条推荐说起typesafe 编码智能体到底在解决什么问题Elvis Saravia 这个名字做 NLP 和提示工程的朋友应该不陌生他长期在整理和分享大模型应用方面的实践经验。他推荐的东西通常不是那种花哨的演示而是能真正落到工程里的思路。这次他提到的 typesafe 编码智能体核心其实就一件事让 AI 写代码这件事从“看起来能跑”变成“结构上不会错”。我刚开始接触编码智能体的时候踩过一个很典型的坑。让模型生成一段 TypeScript 代码它写得有模有样类型标注也齐全但一编译就报错——某个字段的类型对不上某个函数的返回值跟调用方期望的不一致。模型生成的时候“觉得”是对的但它没有真正去验证类型系统能不能通过。这就是 typesafe 编码智能体要解决的核心痛点把类型检查从“事后发现”变成“生成过程中就约束”。所谓 typesafe在编码智能体的语境里不是说模型本身懂类型系统而是整个智能体的工作流里嵌入了类型验证环节。模型生成代码之后不是直接返回给你而是先过一遍类型检查器比如 TypeScript 的 tsc、Rust 的 cargo check、Python 的 mypy 或 pyright。检查不通过就把错误信息回传给模型让它修正循环直到通过或者达到重试上限。这个机制听起来简单但实际落地的时候有很多细节要处理。适合谁来参考这套东西如果你已经在用 Cursor、Claude Code、Codex 这类工具做日常开发但经常被“AI 写的代码编译不过”困扰那这套思路对你直接有用。如果你是团队里负责搭建 AI 编码流程的人想搞清楚 agent harness 和 agent 的区别、MCP 在中间扮演什么角色、模型路由怎么配那这篇内容也能给你一个完整的落地参考。我会从整体设计思路讲到具体实操包括参数怎么选、坑在哪里、怎么排查问题尽量让你看完就能动手试。2. 整体设计与思路拆解为什么是 typesafe为什么需要 harness2.1 编码智能体的核心矛盾生成速度与结构正确性编码智能体最吸引人的地方是生成速度快。你描述一个需求几秒钟之内就能拿到几十行甚至上百行代码。但速度快带来的问题是模型在生成的时候是在做概率预测它并不真正“理解”你的项目结构、类型定义、依赖关系。它生成的代码在语法层面通常没问题但在类型层面、接口层面、依赖层面经常出问题。我实测过一个场景让模型给一个已有的 React 组件添加一个新的 props 字段同时更新所有调用这个组件的地方。模型确实改了但它改的时候没有去读组件本身的类型定义文件而是根据上下文“猜”了一个类型。结果就是组件定义里字段是string | undefined调用方传的是number编译直接报错。这种错误在纯文本层面看不出来只有类型检查器能抓到。typesafe 编码智能体的设计思路就是针对这个矛盾既然模型生成的时候不可靠那就在生成之后加一道类型验证的关卡。这道关卡不是可选的而是强制性的。模型生成的每一段代码都必须通过类型检查才能被采纳。通不过就回炉重造把类型错误信息作为反馈喂回去。这个思路背后的逻辑是类型系统本身就是一种形式化验证。它能在编译期发现的问题就不应该留到运行期。编码智能体如果能把类型检查纳入自己的闭环那它产出的代码质量会有质的提升。2.2 agent harness 到底是什么跟 agent 有什么区别这里需要先把一个概念理清楚因为热搜词里反复出现“harness 和 agent 区别”“harness 到底是啥”。很多人把这两个词混着用但实际上它们指的是不同层面的东西。Agent 是那个“做决策”的部分。它接收你的指令理解你的意图决定下一步做什么——是读文件、是搜索代码、是生成代码、还是调用某个工具。Agent 的核心是决策逻辑它通常由一个大模型驱动配合一套提示词和工具定义。Harness 是那个“管流程”的部分。它不负责决策它负责把 agent 的决策落地成实际的执行步骤并且管理整个执行过程。具体来说harness 要做的事情包括维护对话历史、管理工具调用的输入输出、处理错误和重试、控制执行轮次、记录日志、以及在关键节点插入验证环节。打个比方agent 像是司机harness 像是整辆车。司机决定往哪开但车本身要有方向盘、油门、刹车、仪表盘还要有安全气囊。司机决策失误的时候安全气囊要能弹出来。在 typesafe 编码智能体的场景里类型检查器就是那个安全气囊——它不参与决策但它在 agent 做出错误决策的时候提供保护。我见过很多人只关注 agent 的提示词怎么写忽略了 harness 的设计。结果就是 agent 在简单任务上表现不错一旦任务复杂、需要多轮工具调用就开始出各种问题上下文丢失、工具调用参数错误、死循环、错误累积。这些问题大部分不是 agent 的决策能力问题而是 harness 的流程管理问题。2.3 MCP 在 typesafe 编码智能体里的角色MCP 是 Model Context Protocol 的缩写热搜词里“mcp是什么”“mcp协议”“mcp server”出现频率很高。简单说MCP 是一套标准化的协议让模型能够以统一的方式访问外部工具和数据源。在没有 MCP 之前每个工具都要单独写适配层模型要调用文件系统、数据库、浏览器、设计工具每个都要不同的集成方式。MCP 把这些统一成一套协议模型只需要知道“我要调用某个工具”具体怎么调用由 MCP server 来处理。在 typesafe 编码智能体的场景里MCP 的价值在于它能把类型检查器、编译器、测试运行器这些工具标准化地暴露给 agent。比如你可以写一个 MCP server 来封装 tsc 命令agent 通过 MCP 协议调用它拿到类型检查结果。这样 agent 不需要知道 tsc 的具体命令行参数只需要知道“我要做类型检查”这个意图。热搜词里还有“figma mcp”“蓝湖 mcp”“playwright mcp”“chrome devtools mcp”这些说明 MCP 的生态已经在往各个方向扩展。对于编码智能体来说最相关的 MCP server 通常是这几类文件系统操作、代码执行、类型检查、测试运行、版本控制。如果你要搭建一个 typesafe 编码智能体至少需要把类型检查这个能力通过 MCP 暴露出来。2.4 模型路由为什么不能只用一个大模型模型路由是另一个关键设计点。热搜词里“模型路由”虽然出现次数不多但在实际工程里非常重要。原因很简单不同的任务对模型能力的要求不一样成本也不一样。生成代码这种任务通常需要能力较强的模型比如 Claude 系列或者 GPT 系列的高配版本。但类型检查结果的分析、错误信息的提取、简单文件的读取这些任务用一个小模型就够了。如果所有任务都走大模型成本会很高速度也会慢。模型路由的设计思路是根据任务的类型和复杂度把请求分发到不同的模型。比如代码生成走大模型错误分析走小模型文件摘要走中等模型。这样既保证了关键环节的质量又控制了整体成本。在实际落地的时候模型路由通常由 harness 来管理。Agent 发出一个请求harness 根据请求的类型决定用哪个模型然后把结果返回给 agent。Agent 本身不需要知道背后用的是哪个模型它只需要关注决策逻辑。3. 核心细节解析与实操要点从类型检查到错误回传的完整链路3.1 类型检查器的选择与配置Typesafe 编码智能体的第一步是选一个靠谱的类型检查器。不同语言有不同的选择我按常见场景列一下。TypeScript 项目用 tsc这是官方编译器最权威。配置的时候要注意tsconfig.json里的strict选项建议开启。strict模式会启用一系列严格的类型检查规则包括noImplicitAny、strictNullChecks、strictFunctionTypes等。这些规则能抓到很多模型容易犯的错误比如隐式的 any 类型、可能的 null 值、函数参数类型不匹配。Python 项目用 mypy 或 pyright。mypy 更成熟pyright 更快。如果项目里用了大量的类型标注pyright 的体验更好。配置的时候要注意strict模式以及disallow_untyped_defs这类选项确保模型生成的函数都有类型标注。Rust 项目用 cargo check这个没什么好选的Rust 的类型系统本身就非常严格模型生成的代码很难绕过编译器。这也是为什么 Rust 项目里 typesafe 编码智能体的效果特别明显——编译器会强制模型把类型写对。Go 项目用 go vet 和 go buildGo 的类型系统相对简单但接口和结构体的匹配问题还是能抓到。配置类型检查器的时候有一个关键点要确保检查器的输出格式是结构化的方便 harness 解析。比如 tsc 可以用--pretty false参数输出纯文本错误信息mypy 可以用--no-error-summary去掉汇总信息。这样 harness 拿到错误信息之后可以直接提取文件路径、行号、错误类型然后格式化后回传给模型。3.2 错误信息的提取与回传策略类型检查器输出的错误信息通常很长直接全部回传给模型会占用大量上下文而且很多信息是重复的。我实测下来比较有效的做法是只回传关键信息文件路径、行号、错误码、错误描述。如果同一个文件有多个错误按行号排序只取前 N 个比如前 10 个避免上下文爆炸。回传的格式也很重要。我一般用这样的结构类型检查发现以下错误请修正 文件src/components/Button.tsx 行号23 错误Type number is not assignable to type string | undefined 文件src/utils/format.ts 行号45 错误Property toFixed does not exist on type string这种格式模型很容易理解它知道哪个文件哪一行出了什么问题修正的时候目标很明确。还有一个细节如果模型连续多次修正都通不过类型检查说明它可能陷入了某种误区。这时候 harness 应该介入要么换一个模型重试要么把更完整的上下文比如相关的类型定义文件喂给模型。我一般设置重试上限为 3 次超过 3 次就放弃并报告给用户避免无限循环消耗资源。3.3 MCP server 的搭建要点如果你要通过 MCP 来暴露类型检查能力需要写一个 MCP server。这个 server 的核心功能是接收代码文件路径或代码内容运行类型检查器返回结构化的错误信息。搭建 MCP server 的时候有几个要点。第一要处理好工作目录。类型检查器通常需要在项目根目录运行才能正确解析tsconfig.json或pyproject.toml。MCP server 要能接收工作目录参数或者在启动时指定。第二要处理好超时。类型检查有时候会很慢特别是大型项目。MCP server 要设置合理的超时时间比如 30 秒超时后返回一个明确的错误信息而不是一直卡住。第三要处理好增量检查。如果每次修改都全量检查速度会很慢。可以只检查修改过的文件或者利用类型检查器的增量检查能力。TypeScript 的--incremental选项可以缓存上次检查结果大幅提升速度。第四要处理好错误信息的编码。有些类型检查器的输出包含 ANSI 颜色代码这些代码在回传给模型之前要清理掉否则会干扰模型的理解。3.4 模型路由的配置策略模型路由的配置要根据实际场景来定。我一般把任务分成三类生成类、分析类、执行类。生成类任务包括代码生成、测试生成、文档生成这类任务用能力最强的模型。分析类任务包括错误分析、代码审查、依赖分析这类任务用中等能力的模型。执行类任务包括文件读写、命令执行、结果提取这类任务用最小的模型或者干脆不用模型直接用规则处理。配置模型路由的时候要注意不同模型的输入输出格式可能不一样。Harness 要做一层适配把统一的请求格式转换成各个模型需要的格式再把模型的输出转换成统一的格式返回给 agent。这层适配看起来麻烦但一旦做好后面换模型就很方便。还有一个经验模型路由的配置要可观测。每次路由决策都要记录日志包括请求类型、选择的模型、响应时间、token 消耗。这样你才能知道路由策略是否合理哪些环节成本高哪些环节可以优化。4. 实操过程与核心环节实现搭一个能跑的 typesafe 编码智能体4.1 环境准备与依赖安装先说一下我用的技术栈你可以根据自己的情况调整。我选的是 TypeScript 项目因为类型系统足够丰富能体现 typesafe 的价值。Agent 部分用 Claude 的 APIharness 用 Node.js 写MCP server 也用 Node.js。环境准备的第一步是装 Node.js建议用 20 以上的 LTS 版本。然后初始化项目mkdir typesafe-agent cd typesafe-agent npm init -y npm install anthropic-ai/sdk typescript ts-node types/nodeTypeScript 的配置tsconfig.json里开启 strict 模式{ compilerOptions: { target: ES2022, module: commonjs, strict: true, noImplicitAny: true, strictNullChecks: true, esModuleInterop: true, skipLibCheck: true, forceConsistentCasingInFileNames: true, outDir: ./dist }, include: [src/**/*] }MCP server 的依赖如果你用官方的 SDKnpm install modelcontextprotocol/sdk4.2 类型检查 MCP server 的实现这个 server 的核心逻辑是接收一个文件路径列表运行 tsc解析输出返回结构化错误。import { Server } from modelcontextprotocol/sdk/server/index.js; import { StdioServerTransport } from modelcontextprotocol/sdk/server/stdio.js; import { exec } from child_process; import { promisify } from util; const execAsync promisify(exec); const server new Server( { name: typecheck-server, version: 1.0.0 }, { capabilities: { tools: {} } } ); server.setRequestHandler(tools/list, async () ({ tools: [ { name: typecheck, description: 对指定项目运行 TypeScript 类型检查, inputSchema: { type: object, properties: { projectDir: { type: string, description: 项目根目录 }, files: { type: array, items: { type: string }, description: 要检查的文件列表 } }, required: [projectDir] } } ] })); server.setRequestHandler(tools/call, async (request) { if (request.params.name ! typecheck) { throw new Error(Unknown tool); } const { projectDir } request.params.arguments as { projectDir: string }; try { const { stdout } await execAsync(npx tsc --noEmit --pretty false, { cwd: projectDir, timeout: 30000 }); return { content: [{ type: text, text: 类型检查通过无错误。 }] }; } catch (error: any) { const output error.stdout || error.message; const errors parseTscOutput(output); return { content: [{ type: text, text: formatErrors(errors) }] }; } }); function parseTscOutput(output: string) { const lines output.split(\n).filter(Boolean); const errors: Array{ file: string; line: number; message: string } []; for (const line of lines) { const match line.match(/^(.?)\((\d),\d\):\s*error\sTS\d:\s*(.)$/); if (match) { errors.push({ file: match[1], line: parseInt(match[2]), message: match[3] }); } } return errors; } function formatErrors(errors: Array{ file: string; line: number; message: string }) { if (errors.length 0) return 类型检查通过无错误。; return errors.slice(0, 10).map(e 文件${e.file}\n行号${e.line}\n错误${e.message}).join(\n\n); } const transport new StdioServerTransport(); server.connect(transport);这个 server 的逻辑很直接调用 tsc解析输出格式化错误信息。注意几个细节--noEmit表示只检查不输出文件--pretty false表示不要颜色代码timeout设置 30 秒超时错误信息只取前 10 条。4.3 Agent harness 的核心循环Harness 的核心是一个循环接收用户指令调用 agent 生成代码运行类型检查如果通过就返回结果如果不通过就把错误信息喂回 agent 让它修正。import Anthropic from anthropic-ai/sdk; const client new Anthropic(); interface Message { role: user | assistant; content: string; } async function runAgentLoop(userRequest: string, projectDir: string, maxRetries 3) { const messages: Message[] [ { role: user, content: userRequest } ]; for (let attempt 0; attempt maxRetries; attempt) { const response await client.messages.create({ model: claude-sonnet-4-20250514, max_tokens: 4096, messages: messages.map(m ({ role: m.role, content: m.content })) }); const assistantMessage response.content .filter((block): block is Anthropic.TextBlock block.type text) .map(block block.text) .join(\n); messages.push({ role: assistant, content: assistantMessage }); const typecheckResult await runTypecheck(projectDir); if (typecheckResult.passed) { console.log(第 ${attempt 1} 次尝试通过类型检查。); return assistantMessage; } console.log(第 ${attempt 1} 次尝试未通过错误数${typecheckResult.errors.length}); if (attempt maxRetries) { throw new Error(超过最大重试次数类型检查仍未通过。最后错误\n${typecheckResult.formatted}); } messages.push({ role: user, content: 类型检查发现以下错误请修正\n\n${typecheckResult.formatted} }); } }这个循环的关键在于每次类型检查失败后把错误信息作为新的用户消息追加到对话历史里让模型看到自己上次生成的代码有什么问题然后修正。重试上限设为 3 次超过就报错。4.4 模型路由的接入模型路由的接入点是在调用模型之前根据任务类型选择模型。我一般用一个简单的规则引擎function selectModel(taskType: string): string { const routingTable: Recordstring, string { code-generation: claude-sonnet-4-20250514, error-analysis: claude-haiku-3-5-20241022, file-summary: claude-haiku-3-5-20241022, test-generation: claude-sonnet-4-20250514 }; return routingTable[taskType] || claude-haiku-3-5-20241022; }然后在调用模型的地方把model参数换成selectModel(taskType)的结果。任务类型可以从用户请求里提取或者由 agent 自己判断。模型路由的日志记录也很重要function logRouting(taskType: string, model: string, duration: number, tokens: number) { console.log(JSON.stringify({ timestamp: new Date().toISOString(), taskType, model, duration, tokens })); }这样你就能看到每个任务用了哪个模型、花了多少时间、消耗了多少 token方便后续优化。4.5 完整流程的串联与测试把上面几个部分串起来完整的流程是这样的用户输入请求比如“给 Button 组件添加一个 size 属性支持 small、medium、large 三个值”。Harness 把请求发给 agentagent 生成代码修改。Harness 调用类型检查 MCP server运行 tsc。如果通过返回结果如果不通过把错误信息喂回 agent重复步骤 2-3。超过重试上限报错并返回最后的错误信息。我实测下来对于中等复杂度的修改通常 1-2 次就能通过类型检查。复杂修改可能需要 3 次。如果 3 次还通不过通常是模型对项目结构理解不够需要补充更多上下文。测试的时候我建议从简单的任务开始比如给一个已有函数添加参数、修改一个类型定义、添加一个新的接口实现。这些任务类型检查器能给出明确的错误信息模型也容易修正。等流程跑通了再尝试更复杂的任务。5. 常见问题与排查技巧实录5.1 类型检查通过但运行时报错这是最常见的问题之一。类型检查通过不代表代码逻辑正确。比如模型可能写了一个类型正确的函数但函数体里的逻辑是错的。类型检查器只能验证类型层面的正确性不能验证业务逻辑。排查思路类型检查通过之后再跑一遍单元测试。如果项目里有测试把测试运行也纳入 harness 的验证环节。测试不通过同样把错误信息喂回模型。这样就是双重验证类型检查保证结构正确测试保证逻辑正确。我一般会在 harness 里配置一个验证链先类型检查再跑测试最后跑 lint。三个都通过才算完成。这样虽然慢一点但产出质量高很多。5.2 模型反复犯同一个类型错误有时候模型会陷入一个循环每次修正都引入新的类型错误或者反复犯同一个错误。这种情况通常是因为模型没有理解类型定义的真实意图。排查思路检查回传给模型的错误信息是否足够清晰。如果错误信息只说“类型不匹配”模型可能不知道该怎么改。可以在错误信息里附上相关的类型定义让模型看到完整的上下文。比如如果错误是Type number is not assignable to type string可以把那个string类型的定义也附上让模型知道为什么必须是 string。另一个技巧是换一个模型重试。不同模型对类型系统的理解程度不一样有时候换个模型就能解决。5.3 MCP server 连接失败MCP server 连接失败通常有几个原因路径不对、权限不够、依赖没装、端口冲突。排查步骤先确认 MCP server 的可执行文件路径是否正确然后手动运行一下 server看能不能正常启动。如果启动报错通常是依赖问题。如果启动正常但 agent 连不上检查 MCP 的配置文件确认 server 的启动命令和参数是否正确。我踩过的一个坑是MCP server 的工作目录设置错了导致 tsc 找不到tsconfig.json。解决办法是在 MCP server 启动时显式指定工作目录或者在调用工具时传入项目根目录。5.4 上下文爆炸导致模型响应变慢多轮重试之后对话历史会越来越长模型的响应速度会变慢成本也会增加。特别是错误信息很长的时候上下文增长很快。排查思路限制回传的错误信息长度只取前 N 条。同时可以在重试几次之后把之前的对话历史压缩一下只保留最近一次的错误信息和修正结果。我一般设置上下文上限为 8000 token超过就截断最早的对话。还有一个技巧把类型检查的错误信息做摘要只保留错误类型和文件位置去掉详细的错误描述。这样能大幅减少 token 消耗。5.5 常见问题速查表问题现象可能原因排查方法解决措施类型检查通过但运行报错逻辑错误类型检查抓不到跑单元测试把测试纳入验证链模型反复犯同一错误错误信息不清晰检查回传的错误信息附上相关类型定义MCP server 连不上路径/权限/依赖问题手动运行 server检查配置和依赖响应变慢、成本增加上下文爆炸检查对话历史长度限制错误信息长度压缩历史重试多次仍不通过模型理解不够检查任务复杂度补充上下文或换模型类型检查超时项目太大检查项目规模增量检查或只检查修改文件5.6 几个我踩过的坑和对应的技巧第一个坑一开始我没有限制错误信息的条数结果一个文件有几十个错误全部回传给模型上下文直接爆了。后来改成只取前 10 条效果好很多。而且模型修正的时候通常修完前几个错误后面的错误也会跟着消失。第二个坑我一开始用tsc的全量检查大型项目要跑十几秒。后来改成只检查修改过的文件用--incremental选项速度提升到 2-3 秒。这个优化对体验提升很大。第三个坑模型有时候会“过度修正”把本来正确的代码也改了。解决办法是在回传错误信息的时候明确告诉模型“只修正报错的部分不要改动其他代码”。这个提示词很关键。第四个坑MCP server 的输出格式不稳定有时候有颜色代码有时候没有。后来我在 server 里统一做了清理确保输出是纯文本。这个细节看起来小但影响很大。6. 扩展方向与个人体会这套 typesafe 编码智能体的思路不仅适用于 TypeScript也适用于任何有类型系统的语言。Rust 项目里效果尤其明显因为 Rust 编译器非常严格模型生成的代码几乎不可能绕过类型检查。Python 项目里如果用 mypy 严格模式效果也不错。后续可以扩展的方向有几个。一是把更多的验证工具纳入 harness比如安全扫描、性能分析、依赖检查。二是把模型路由做得更细根据代码的复杂度动态选择模型。三是把 MCP server 做得更通用支持多种语言和多种检查器。我个人在实际操作中的体会是typesafe 编码智能体的核心价值不在于模型有多强而在于验证环节有多可靠。模型再强也会犯错但一个可靠的验证闭环能保证错误被及时发现和修正。这比追求模型能力提升要实在得多。最后分享一个小技巧如果你刚开始搭这套东西不要一上来就追求全自动。先手动跑通“生成-检查-修正”这个循环确认每个环节都能正常工作再逐步自动化。这样出问题的时候容易定位也容易调整。