☰
Typesafe编码智能体实战:用类型检查闭环提升AI代码质量
2026/10/17 2:05:27 网站建设 项目流程

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,为什么需要 harness

2.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 build,Go 的类型系统相对简单,但接口和结构体的匹配问题还是能抓到。

配置类型检查器的时候有一个关键点:要确保检查器的输出格式是结构化的,方便 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 的 API,harness 用 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/node

TypeScript 的配置,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 的依赖,如果你用官方的 SDK:

npm install @modelcontextprotocol/sdk

4.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\s+TS\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: Record<string, 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 完整流程的串联与测试

把上面几个部分串起来,完整的流程是这样的:

  1. 用户输入请求,比如“给 Button 组件添加一个 size 属性,支持 small、medium、large 三个值”。
  2. Harness 把请求发给 agent,agent 生成代码修改。
  3. Harness 调用类型检查 MCP server,运行 tsc。
  4. 如果通过,返回结果;如果不通过,把错误信息喂回 agent,重复步骤 2-3。
  5. 超过重试上限,报错并返回最后的错误信息。

我实测下来,对于中等复杂度的修改,通常 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 编码智能体的核心价值不在于模型有多强,而在于验证环节有多可靠。模型再强也会犯错,但一个可靠的验证闭环能保证错误被及时发现和修正。这比追求模型能力提升要实在得多。

最后分享一个小技巧:如果你刚开始搭这套东西,不要一上来就追求全自动。先手动跑通“生成-检查-修正”这个循环,确认每个环节都能正常工作,再逐步自动化。这样出问题的时候容易定位,也容易调整。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询