我早期用 AI 写代码时,习惯把需求直接发给模型,然后看着它一路往下写。做小脚本、改一个页面,这种方式很舒服,反馈快,也不需要准备太多东西。

项目开始长期迭代以后,问题会慢慢出现。模型知道当前这句话,却未必知道这个模块为什么这样拆、以前做过哪些取舍,也不一定记得上一个会话停在哪里。几轮修改下来,代码可能还能运行,结构和约束却逐渐变得模糊。

我在其他项目里使用 Trellis 后,开始把 LLM 协作开发整理成 Spec Coding 的工作方式。更准确地说,这是一种 Harness Engineering 实践:把项目规范、任务材料、工作状态、上下文注入、检查和经验记录组织到一起,让模型每次接手任务时都有一份可以读取的项目背景。

我怎么理解 Harness Engineering

我会把 Agent 的工作环境拆成两层。

模型负责理解需求、推理、生成代码和解释结果;Harness 负责把模型放进一个具体的项目里,告诉它当前项目有什么约束、正在处理哪个任务、应该读取哪些资料、可以调用哪些工具,以及完成以后要怎样检查和记录。

如果只有模型,得到的通常是一段针对当前对话的回答。加上项目级 Harness 后,模型才有机会沿着项目既有的规范和状态继续工作。AGENTS.mdspec/、task、Hook、workflow-state、测试命令和 journal,都属于这层工程结构。

Trellis 的价值正在这里。它把一次次对话需要依赖的项目事实放进仓库和运行流程里,再按当前任务动态提供给模型。和单纯拉长 Prompt 相比,这种方式更容易长期维护。

从文件结构看,Trellis 也没有把所有规则塞进一个说明文件,而是按生命周期把项目材料拆开。一个初始化后的项目大致会是这样,具体任务文件和平台目录会根据实际使用情况生成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
项目根目录/
├── AGENTS.md # 通用项目指引
├── .trellis/ # Trellis 的跨平台核心
│ ├── .developer # 当前开发者标识
│ ├── .runtime/ # 会话和 active task 状态
│ │ └── sessions/
│ ├── spec/ # 长期规范和可复用经验
│ │ └── .../index.md
│ ├── tasks/ # 当前任务及其材料
│ │ └── <MM-DD-name>/
│ │ ├── task.json
│ │ ├── prd.md
│ │ ├── design.md # 复杂任务按需创建
│ │ ├── implement.md # 人可读的实施计划
│ │ ├── implement.jsonl # 实现阶段的上下文清单
│ │ ├── check.jsonl # 检查阶段的上下文清单
│ │ └── research/ # 持久化调研记录
│ ├── workflow.md # 阶段定义和 workflow-state
│ └── workspace/ # 按开发者隔离的工作记忆
│ └── <developer>/
│ ├── index.md
│ └── journal-N.md
├── .claude/ # Claude Code 适配层
├── .codex/ # Codex 适配层
└── ... # Cursor、OpenCode 等平台目录

.trellis/ 是团队可以共享的核心。spec/ 保存长期规则,tasks/ 保存当前任务,workspace/ 保存开发者自己的 journal 和跨会话 notes;.runtime/workflow.md 则负责把当前任务和所在阶段接回运行流程。不同平台只需要配置自己的适配层,项目规范和任务材料仍然来自同一套文件。

模型能力和 Harness 各自解决什么问题

我后来越来越明确,模型能力和 Harness 解决的是两个层面的问题。

模型越强,通常越能理解任务阶段,读懂相关 spec,也更愿意按照约定使用工具。需求对齐、规划和实现之间的来回次数会少一些,整个流程用起来更顺手。

但模型本身仍然会受到上下文长度、当前会话信息和工具返回结果的影响。即使是能力很强的模型,也可能漏读一条规则,误解一个边界,或者在工具调用失败后继续沿着错误假设往下写。单靠模型当下的判断,很难把这些波动稳定压住。

Harness 提供的是一层外部的工程约束和恢复路径。规范放在 spec 里,任务状态放在 task 和 workflow-state 里,Hook 负责按轮次补充上下文,check 负责验证结果,journal 记录过程中发生过什么。模型换了、会话重新开了,或者某次工具调用出了问题,开发仍然有地方可以回看和修正。

这套结构不会让能力较弱的模型突然变得可靠,也不能保证每次工具调用都成功。它能做的是把错误暴露在任务流程里,减少错误继续扩散的机会,让开发的基本秩序不随着某个模型的状态大幅变化。

所以我现在更愿意这样理解两者的关系:Harness 把稳定性托住,模型能力决定执行过程有多顺畅。模型越强,越能把这套流程用得轻快;流程本身仍然应该存在。

新任务先从对齐和脑暴开始

我现在不会一看到需求就让模型直接写代码。先花一点时间把想法摊开,确认这次到底要解决什么问题。

通常会一起讨论这些内容:

  • 使用者是谁,具体场景是什么。
  • 这次要解决的核心问题是什么。
  • 哪些能力属于本次范围,哪些暂时不做。
  • 现有代码、接口和架构有哪些限制。
  • 怎样算完成,验收要看哪些结果。
  • 可能遇到哪些异常和边界情况。

这里的脑暴不需要变成一份很长的会议纪要。它更像是把模糊需求来回掰开,等目标、范围和约束基本稳定后,再决定要不要进入完整任务流程。

小问题可以直接在当前对话里解决,例如一个局部 Bug 或一个很小的文案调整。涉及多个文件、多个模块,或者后面还需要继续推进和复盘的事情,才值得创建 task。

Spec 负责保存项目的长期背景

Trellis 会把项目资料放进 .trellis/。其中,spec/ 更接近项目的长期事实源,适合记录架构约束、接口约定、前后端规范、测试习惯和领域规则。

我不会让模型每次把整个 spec/ 目录都读一遍。当前任务涉及后端,就读取相关后端规范;涉及页面,就读取前端和交互约定。按任务选择上下文,既能让模型更贴近项目,也能避免一份越来越长的规则文件把真正重要的内容淹没。

和 spec 对应的还有两类材料:

  • task 记录当前要做的事情,包括需求、设计、实施清单和调研结果。
  • journal 记录这次开发做到哪里、遇到什么问题,以及之后需要注意什么。

项目规范、当前任务和个人工作记忆分开后,模型更容易知道哪些内容需要长期遵守,哪些内容只和眼前这次改动有关。

这套流程也在保护项目的可维护性

在我自己的项目里,Harness 对可维护性和扩展性的帮助很明显。每个任务都要对照现有 spec、task 和真实代码,新增功能不能只围着眼前的调用路径打补丁,还要交代它和现有模块的关系、边界以及后续的测试方式。这样做不会自动消除技术债,却能减少重复逻辑、临时分支和“先跑起来再说”的局部修补。时间久了,模块边界更容易保持,后续扩展也有可以参考的入口,项目不至于在一次次赶进度中堆成一座没人敢动的“屎山”。

这也让我意识到,spec 的文档治理本身就是一项工程工作。写得太少,模型缺少判断依据;写得太多,关键规则很快就会被淹没;代码已经变化,spec 却停留在旧状态,过时文档还可能把模型带到错误方向。

我现在会把稳定、能跨任务复用的规则放进 spec,把当前任务的方案、临时限制和未决问题留在 task 里。重要的 spec 变更需要经过 review,代码、测试和文档尽量在同一次变更中保持一致,定期还要清理已经失效的条目。spec 更像一份需要随着代码一起维护的工程资产,不能在项目建好时写完就搁在那里。它和代码、测试保持同步,后续功能才有可靠的边界;长期不维护,文档本身就会变成另一种技术债。

Hook 让每一轮对话都带着当前状态

只在会话开始时加载一次上下文,仍然不够。实际开发往往会经历好几轮对话:补充需求、确认方案、继续实现、修复问题、运行检查。中间如果状态丢了,模型就可能在需求还没有确认时开始改代码,或者代码完成后忘记检查和收尾。

Trellis 在支持 Hook 的环境里,会在会话启动时恢复项目上下文,并通过 UserPromptSubmit 在每轮用户消息进入时注入当前的 workflow-state。这部分就是 Harness 的运行时机制:模型可以看到当前项目、开发者、活跃任务和所在阶段,再判断这一轮应该继续澄清、执行、检查,还是收尾。

我觉得这部分很实用。模型并没有因此获得真正的长期记忆,项目需要记住的内容仍然放在文件里;Hook 做的是每次把相关状态带回来,减少模型对聊天历史的猜测。

如果使用 Codex,还要确认当前环境已经启用 Hook,并完成对应的审批。不同工具和版本的接入方式会变化,具体以当前 Trellis 和工具文档为准。Hook 没有生效时,仍然可以通过仓库里的 AGENTS.md 和启动流程调用相关skill手动读取上下文,只是连贯性会差一些。

Planning 把一句需求变成任务材料

确认进入完整流程后,Trellis 会在 tasks/ 下建立任务目录。需求、设计、实施清单和调研记录都放进去,后续换会话时,模型可以直接读取这些材料,不需要从聊天记录里猜当初的想法。

比如一个“增加登录功能”的需求,真正开始写代码前,还要确认登录失败如何反馈、Token 如何过期、旧用户数据如何兼容、哪些接口需要权限校验。Planning 阶段把这些问题显式写下来,后面的实现就有了依据,讨论也不容易在几轮对话后散掉。

这一步对我来说还有一个额外价值:它会留下产品和技术决策的痕迹。过一段时间回看时,能知道当时为什么选择某个方案,哪些方案被排除过。

Execute:按照 task 和相关 spec 开发

进入实现阶段后,模型参考的内容不只是一条最新 Prompt。它需要同时看到:

  1. 当前的 workflow-state。
  2. 任务里的需求、设计和实施清单。
  3. 当前功能涉及的项目 spec。
  4. 仓库中的真实代码、接口和已有测试。
  5. 这一轮新增的具体要求。

这套上下文顺序能让模型更贴近现有项目。比如要修改一个接口,它不仅要知道“新增字段”,还要知道项目统一的错误格式、目录边界和测试要求。对于前端任务,也需要遵守已有的组件和交互约定。

Arona 和 Paperwhisper 给我的提醒

Arona WebUI 的开发让我意识到,协议理解、页面信息架构和功能优先级需要同时考虑。模型可以帮忙写 Node.js 服务、前端模块和 WebSocket 处理逻辑,但网关消息是否符合协议、哪些运维信息应该放在首屏,仍然需要我自己判断。

Paperwhisper 则让我更早地把产品目标写清楚。拟物化视觉、Flutter 页面和数据保存都可以让 LLM 协助实现,用户进入写作状态时会不会被打扰,还是要回到真实场景去检查。

这两个项目让我更重视“开发前留下什么材料”。在后续长期维护的项目里,我会把这些判断放进 task 和 spec,让下一次会话里的模型有东西可读,也让未来的自己能接着做。

Check:检查标准要来自项目约定

写完代码后,单纯运行一次 lint 和 test 往往不够。有些问题不会立刻导致测试失败,比如接口错误格式不统一、权限校验漏掉、目录依赖方向反了,或者实现绕开了项目原有的架构约束。

Trellis 的 check 阶段会查看当前 diff,并对照任务里声明的 spec 和 research 记录进行复核。这样一来,检查标准来自项目材料和任务目标,不只依赖开发者当下的感觉。

我通常会额外确认:

  • 接口参数是否和真实文档一致。
  • 空数据、重复请求和网络断开如何处理。
  • 新增依赖是否真的有必要。
  • 关键路径有没有可复现的测试方式。
  • 文档和实现是否已经同步。

Update Spec 和 Journal:把经验留在项目里

任务结束时,Trellis 会引导判断这次有没有稳定、可复用的经验值得写回 spec。

适合进入 spec 的内容通常是长期规则,例如某类接口必须使用统一错误格式,某个模块不能直接依赖另一个模块,某类数据库迁移必须提供回滚说明。

只对当前任务有效的内容,继续留在 task 里就够了。比如某个页面这次使用了一个具体路径,下一次不一定还适用,把所有细节都塞进 spec,反而会让它越来越难读。

业务代码提交完成后,再运行 finish-work 做任务归档和 journal 记录。业务代码的提交归业务代码,Trellis 的任务状态和工作记忆归 Trellis 管理,边界分开后比较清楚。

我会在什么场景使用这套流程

短脚本、一次性验证和很小的局部修改,直接对话开发通常更省事。项目需要长期维护、涉及多个模块、业务规则比较多,或者经常在 Claude Code、Codex、Cursor 之间切换时,Spec Coding 的价值会明显一些。

Trellis 也有成本。spec 需要维护,task 需要控制粒度,Hook 和不同工具的适配也需要检查。如果项目本身没有长期维护计划,硬套完整流程只会留下空模板。

对我来说,Trellis 的意义在于给 LLM 一套持续可读取的项目环境:需求有记录,规范有出处,当前阶段有状态,做完以后还有经验可以回流。模型写代码的速度仍然重要,项目能不能在几个月后继续接上,也需要在开发当下被考虑。这正是我理解的 Harness Engineering:让模型的能力落到一个有上下文、有边界、可检查的工程系统里。

参考:Trellis 官方文档