
最近我把大部分业余时间都花在了一个叫 t3code 的命令行工具上。名字里的 T3 不是 T3 Stack 那个 TypeScript Tailwind tRPC 的 T3而是我自己定义的三个词Type-safe、Test-driven、Trusted——类型安全、测试驱动、变更可信可复现。说白了我想要的不是一个更聪明的代码补全器而是一个能自己在终端里读代码、改代码、跑测试然后带着 diff 回来找你确认的编码执行器。如果你已经厌烦了在编辑器里来回切换窗口复制粘贴错误信息或者对那种“改了十来个文件结果编译直接挂掉”的 AI 重构体验深有体会那么这个工具的设计思路应该对你有用。文章适合三类人想了解终端 AI 编码工作流的开发者、准备自己搭 agent 工具的动手派以及单纯想看看“类型安全 测试闭环”到底能解决什么问题的围观群众。我不会把 t3code 包装成什么成熟产品它目前就是个跑在 Node 下的实验性 CLI但里面的思路已经在我两个真实项目上验证过一轮值得拿出来聊聊。1. 为什么我会自己写一个终端编码工具从“补全”到“带目标地执行”AI 编程工具这几年的形态变化很有意思。最开始大家都堆编辑器里的内联补全后来聊天框里能贴代码了再后来出现了能在终端里跑任务的 agent。工具越来越“主动”但一个尴尬的事实是大多数人的日常工作流其实仍停留在“补全 粘贴”这个阶段。这不是用户懒而是补全和粘贴这两种形态的天花板明显。1.1 三种主流形态各自卡在哪编辑器内联补全我到现在都还在用它在“你正写到一半、脑子里已经有了下一行”的场景里效率最高。可一旦任务变成“把用户模块的加载逻辑迁到一个自定义 hook 上”它就彻底使不上劲了——因为这类改动跨文件、依赖类型定义、还必须在改完后验证行为没变补全模型只看得到你光标前面的几百个 token。对话式 AI 擅长单点输出比如“给我写一个带重试的 fetch 封装”。但它有个致命问题代码离开聊天窗口后没有人替你验证它能不能编译、能不能跑通。你得手动建文件、装依赖、处理报错再把报错粘回对话框循环往复。偶尔一次还好任务一多就变成了纯体力活。终端里的任务型 agent 是这三类里我最看好的因为它围绕“目标”工作而不是围绕“字词”工作你说“把软删除加上”它自己去读代码、规划、改文件、跑测试。但这类工具的默认实现有一个普遍短板——模型在靠“常识”写代码而不是靠项目里最可靠的事实来源类型定义和测试用例。我见过大量 agent 失败案例失败原因不是模型笨而是它根本看不到 tsc 的报错改完也不跑测试最后交回来一个“看起来对但一编译就挂”的烂摊子。1.2 t3code 的切入点让模型先看到事实再动手t3code 的起点就是补上这个短板在终端 agent 的工作循环里把类型检查、测试执行、diff 审查做成强制步骤而不是可选项。任务进来先做环境探测改动之前先读类型约束改完之后必须过 tsc 和测试没过就把失败信息回填给模型继续修。这套东西听起来不复杂但把“AI 生成代码”这件事从“猜”变成了“验证驱动”。另一个选择是为什么做成终端工具而不是 IDE 插件。终端天然适合做可审计的工作流每一步操作都能被记录、回放可以集成进 CI也可以导出成日志给同事看。IDE 插件当然体验更顺滑但“鼠标点完就不知道发生了什么”的工作方式对要跑在无人值守场景下的编码工具来说是个致命伤。做终端工具的成本也低一个小项目只需要 Node、一个 CLI 入口不需要适配各种编辑器 API。2. t3code 的核心设计把类型安全当作一等公民这一节讲设计层面。t3code 不是一个大模型封装它更像一个“会使用 tsc、vitest、git 的机器人操作员”。2.1 一条命令的完整生命周期在终端输入t3code run 把用户列表页的 useEffect 数据加载逻辑迁移到 useApiQuery渲染结果不变vitest 通过t3code 会按下面这条流水线执行。阶段主要动作产出环境探测读 package.json、tsconfig、pnpm-workspace检查 git 状态与 Node 版本项目事实清单上下文组装按需读取相关文件构建 token 预算初始上下文规划模型给出改动方案与文件清单任务计划编码循环读文件、写文件、跑 tsc / vitest、回填失败信息通过验证的改动结果汇报输出 diff --stat、完整 diff、测试结果需要人确认的变更集你可能会问为什么最后一步不直接自动提交原因后面会具体讲但核心是可信需要人审。自动提交完全可以但必须保证“审计成本 节省的时间成本”一旦模型改动的文件数超过你的 review 能力自动提交就是给自己埋雷。这个判断来自一次真实事故后面第 5 节会展开说。2.2 类型信息为什么是“免费的信号”这是 t3code 最重要的一个设计判断编译错误是最好的反馈信号而且它完全免费。模型生成代码时最常见的失败模式就是瞎猜某个函数的参数类型或者在一个联合类型字段上直接调用了不存在的方法。这类错误人类一眼能看出来但模型看不见除非你主动把错误信息喂给它。tsc --noEmit 的输出恰好是结构化、可解析、可截断的。我把错误信息按“文件:行号:列号 - 错误描述”标准化之后直接拼进下一轮对话的上下文。模型看到“UserList.tsx:42 - Argument of type X is not assignable to parameter of type Y”大部分时候下一轮就能给出精准修复而不是重写整个文件。这就是“让模型先看到自己的失败再让它说话”。Type-safe 原则的完整含义是项目里已经存在的类型约束必须无条件成为 AI 修改代码时的参考基准。模型不该发明类型应该适配类型。这也是为什么 t3code 的上下文组装器会把 TypeScript 相关的配置和近邻文件的类型定义当成最高优先级读取对象而不是让模型从海量文件里碰运气。2.3 Test-driven 和 Trusted 落到代码上Test-driven 在 t3code 里的含义很朴素任务描述必须包含验收标准验收标准最终会变成至少一条可执行的测试命令。没有验收标准的任务t3code 会反问你要“什么样的结果算完成”答不上来就不开工。这个“反问”功能最初只是为了省我的事后来发现是整条工作流里性价比最高的一段代码。Trusted 原则体现在三件小事上每次文件修改都会生成可回滚的 diff绝不原地覆盖。所有执行过的命令和读写过的路径都会写进任务日志。写操作分“局部副作用”和“全局副作用”全局副作用比如安装依赖默认要人工确认。这三件事单独拿出来都不稀奇合在一起的效果是这台机器人每次干完活你都能回答“它动了什么、为什么动、能不能回滚”。换句话说t3code 不是让 AI 替你拍板而是让 AI 在极其透明的约束下帮你执行最终拍板的依然是你。这个定位决定了它和那些“一键全自动”工具的理念差异。3. 关键模块的实现链路从提示词组装到沙箱验证设计再好落不了地也是白搭。这一节我把 t3code 最核心的四个模块逐个拆开说说实现思路和踩过的坑。3.1 上下文组装不把一个仓库都塞给模型一开始我图省事直接把仓库文件列表和 README 全塞给模型token 爆炸不说模型还容易被无关信息带偏——它看到一百个文件就倾向于“雨露均沾”地乱改。后来我改成“按需阅读”策略初始上下文只包含环境探测阶段拿到的项目事实package.json 里的脚本、tsconfig 关键选项、工作区结构、测试命令。接下来使用一个只读工具 grep_search 定位任务涉及的符号再顺着 import 关系读近邻文件。每个读过的文件都会放进一个 LRU 缓存同一文件重复读取不重复消耗 token。token 预算我分成三块项目事实、任务描述、运行日志。任何一块超限先截断最旧的运行日志绝不截断任务描述——因为模型一旦“忘了”任务目标后面所有修改都会跑偏。这里有个真实教训有一次运行日志里积压了二十多条 vitest 输出模型开始对着一条无关的 deprecation 警告研究起来完全忘了任务是加软删除。从那以后日志块只保留最近四轮的结果。3.2 工具协议稳定、可审计、可回滚模型和仓库交互的全部能力都抽象成一个工具集合每个工具必须满足同一份接口描述interface Tool { name: string; description: string; inputSchema: JSONSchema; sideEffects: none | local | global; }当前实现里有六七个工具read_file、write_file、grep_search、list_directory、run_command、git_diff。前四个是“无副作用或局部副作用”run_command 是“全局副作用”——凡是声明为 global 的命令t3code 在真正执行前会把完整命令打印出来除非你加了 --yes否则必须手动确认。为什么这么设计因为模型很容易在“安装依赖”这种看似无害的操作里给你的全局环境留下东西。pnpm 装包、npm 升级、修改 shell 配置这些行为一旦跑在无人值守模式下事后排查成本远高于当场确认一次。工具层做统一描述也是为了让将来接入 MCPModel Context Protocol时不用重写框架——工具协议和 MCP 的工具模型本来就很接近。3.3 沙箱验证在不动你仓库的前提下让代码跑起来改完代码必须验证但在原仓库里直接跑测试有几个隐患模型可能在 package.json 里装了一堆临时依赖可能把配置文件改坏也可能触发了 Next.js 的缓存导致“改了代码但行为没变”的幻觉。我的方案是给每次任务创建临时工作区。实操上我用 git worktree 从当前 commit 拉一个干净目录把模型改动的文件同步过去再在临时目录里跑 tsc 和 vitest。这个方案有两个已知的坑。第一个是 monorepopnpm workspace 的软链结构在临时目录里经常行为不一致必须先读根目录的 pnpm-workspace.yaml 并重建 workspace。第二个是缓存Next.js 的 .next、Vite 的 node_modules/.vite 这类目录必须排除否则验证的是缓存而不是你的代码。命令大概长这样示意不涉及具体仓库git worktree add ../t3code-verify HEAD # 同步模型改写过的文件到临时目录 # 在临时目录中执行 pnpm install --frozen-lockfile # 执行 pnpm tsc --noEmit pnpm vitest run git worktree remove ../t3code-verify --force3.4 失败回填编译错误与测试输出的“二次利用”模型不是一次就能改对的这很正常关键在于失败之后怎么处理。t3code 会捕获 tsc 和 vitest 的完整输出然后执行一次清洗过滤掉 deprecation 警告、第三方包内部的报错、以及“文件:行号”格式无法匹配的冗余信息只保留最近 40 行有效输出拼成“验证事实”送回模型。这个清洗步骤非常关键。原始输出里混着大量噪音模型如果盯着一条无关警告死磕会浪费好几轮。清洗之后模型看到的是“UserList.tsx:42 类型不匹配”“Pagination.test.tsx 第 3 个用例失败期望 0 收到 1”这种可以直接行动的信息。我还加了一个失败策略切换同一类错误连续出现两次就暂停当前修复路径强制模型先重新读相关文件、重新规划再继续改。比如它连续两次把 enabled 字段传成布尔值而不是对象属性第三次就不会让它继续在这个方向上打补丁而是要求它先把 useApiQuery 的类型定义完整读一遍再动手。避免“用同一个错误方案撞墙两次”是最基本的止损手段。4. 实测复盘一个重构任务从下命令到合并的全过程光讲设计是不够的我拿一个真实跑过的任务来复盘。项目是一个中等规模的 Next.js tRPC Prisma 应用用户列表页和用户详情页共用一套基于 useEffect 的数据加载逻辑而项目里其实已经封装好了 useApiQuery 这个自定义 hook。我要验证的就是 t3code 能不能自动完成这次迁移。命令是t3code run 把 UserList.tsx 与 UserDetail.tsx 的 useEffect 数据加载逻辑迁移到 useApiQuery对外渲染行为不变tsc --noEmit 与 vitest 全部通过diff 尽量小不修改 eslint 配置4.1 前两轮读代码、定方案、撞上类型约束第一轮t3code 用 grep_search 定位了 useEffect 的所有出现位置确认两个文件确实在用同一套逻辑。它给出的方案是抽到公共逻辑里两个页面共用 useApiQuery方案本身符合我的预期。规划阶段它没有急着改代码而是先把两个文件的 import 区域和对应 hook 的引用情况列了出来这个行为模式很重要——好的 agent 应该先画清楚改动范围而不是直接开写。第二轮它就直接动手了。UserList.tsx 被整体重写乍看逻辑没问题但 tsc 报了一个错useApiQuery 要求传入带 enabled 字段的 options而原逻辑在条件不满足时直接不发请求。第一版编译失败。注意这个失败其实是好事因为模型在第一轮根本没有读 useApiQuery 的类型定义它只是凭函数名和形式上推断tsc 的报错给了它精确的修正方向第二轮这个失败直接省掉了后面潜在的运行时 bug。4.2 第三轮最小修复与“无关 diff”的翻车第三轮里模型没有重写整个文件而是在 options 里补了 enabled: false。为什么行为这么收敛我复盘发现上下文里携带了“上一轮 diff”作为近期修改记录模型能清楚看到自己上一轮改了哪些行从而倾向于最小改动。这一点非常重要diff 回填是对抗“模型爱乱改”最有效的手段。但这一轮也翻车了模型顺带改了 UserDetail.tsx 里的一个样式类名。原因是在 grep 结果里它看到了这个类名和加载逻辑出现在同一个文件中下意识觉得“顺手一起改了吧”。这正是我坚持在结果汇报阶段必须展示每个文件的 diff 行数的原因——光看总体 diff 很难发现一个不该动的文件被改了 5 行。我在 review 时一眼抓住手动还原了那一处。这类无关改动不会让测试失败但会污染 git 历史只能靠人在最后把关。4.3 与补全、对话式工具的体感差异这类任务用编辑器内联补全基本无解因为改动横跨两个文件、依赖外部 hook 的类型签名。用对话式工具能做但你需要手动把两个文件的相关代码、hook 的类型定义、编译报错一遍遍粘贴过去改一轮至少五分钟而且过程中你始终是“那个负责夹在 AI 和编译器之间的中间人”。t3code 的整个流程我没有干预只在最后 review。我认为体感差别最大的不是速度而是失败变得可见了它不会把改错的东西直接盖在你代码上而是先放在临时目录里跑验证验证不过就自己修修完再把 diff 拿给你看。对于一个对代码质量有要求的人来说这个“隔着一层沙箱”的距离感比“模型在你工作区里直接写文件”要安心太多。我也跑过失败的案例。一次涉及 monorepo 的任务t3code 没先读根目录的 pnpm-workspace.yaml按照单一 package 的方式处理结果临时目录里依赖安装失败白费了三轮。这个教训后来直接变成了环境探测阶段的一项强制逻辑检测到 workspace 文件就优先重建 workspace再谈其他。5. 目前踩过的坑与正在做的三件事工具没有被吹到天上它现在的成熟度大概相当于“一个比较靠谱的初级开发需要 code review”。继续用的过程中我又攒了几个坑。5.1 token 噪音与“死磕同一条错误”模型窗口够大不代表上下文质量高。运行日志里塞满第三方库的 deprecation 警告时模型的表现会明显下降甚至做出“研究警告”这种偏离任务的事情。我现在对输出做两级过滤先按文件路径白名单去掉 node_modules 和缓存目录内的信息再按“error / fail / expected”这类关键词提取有效行。过滤之后模型收到的每一条信息都值得它花一次注意力。死磕问题也是真实存在的同一编译错误连续出现时模型会下意识换个写法再试一次然后继续死磕。t3code 的失败策略切换解决了这个问题的另一半连续两次看到同类失败就必须回到“重新读文件、重新规划”的暂停状态而不是继续在同一个方案上打转。这种强制暂停看起来打断了流畅性但长远看反而省 token因为模型不再用同一套错误逻辑反复撞击同一面墙。5.2 为什么我保留了手动确认而不是全自动我试过 --yes 模式体验确实爽直到某次任务里模型把 package.json 的 devDependencies 重排了合并后 git blame 一片混乱另一次它删了一个看起来“没被引用”的导出函数结果一个运行时才加载的模块直接崩了。这两件事提醒我全自动适合“只改单文件且测试全过”的极简场景不适合任何涉及配置或跨模块的任务。现在的默认策略是改动文件数超过 5 个时必须分批确认涉及 package.json 的修改永远确认只有改动文件少于等于 3 个且验证全过的任务才允许 --yes。这个策略不酷但很稳。工具的价值在于让人少做重复劳动而不是让人完全退出决策全自动的收益再大也比不上一次线上事故的代价。5.3 下一步要做的事我正在改三件事。第一件是项目档案在 .t3code/project.json 里缓存技术栈、常用命令、测试入口、工作区结构让后续任务免去反复探测。第二件是重放日志每次任务的输入、模型输出、验证结果全部记录成 JSONL方便复现问题和在 CI 上做回归。第三件是 MCP 互通既然工具协议已经长得像个简化版 MCP不如直接注册成 MCP server让其他支持 MCP 的终端 agent 也能调用 t3code 的读文件和验证工具不把自己困在小生态里。最后分享一个我在实际使用中体会最深的东西任务描述里只要带上了可验证的验收标准成功率会高一个量级。“给用户列表加一个删除按钮”和“给用户列表加删除按钮点击后该用户从列表移除并弹出成功提示vitest 通过”对 agent 来说是两种完全不同的任务。前者它只能猜后者它有明确的终点。我现在写任务描述时至少会问自己一句做完以后我用什么命令能立刻知道它做对了这个问题问清楚了t3code 这类工具才能真正跑起来。