从临时对话到 Harness Engineering:我如何用 Trellis 组织 LLM 协作开发
我早期用 AI 写代码时,习惯把需求直接发给模型,然后看着它一路往下写。做小脚本、改一个页面,这种方式很舒服,反馈快,也不需要准备太多东西。
项目开始长期迭代以后,问题会慢慢出现。模型知道当前这句话,却未必知道这个模块为什么这样拆、以前做过哪些取舍,也不一定记得上一个会话停在哪里。几轮修改下来,代码可能还能运行,结构和约束却逐渐变得模糊。
我在其他项目里使用 Trellis 后,开始把 LLM 协作开发整理成 Spec Coding 的工作方式。更准确地说,这是一种 Harness Engineering 实践:把项目规范、任务材料、工作状态、上下文注入、检查和经验记录组织到一起,让模型每次接手任务时都有一份可以读取的项目背景。
我怎么理解 Harness Engineering
我会把 Agent 的工作环境拆成两层。
模型负责理解需求、推理、生成代码和解释结果;Harness 负责把模型放进一个具体的项目里,告诉它当前项目有什么约束、正在处理哪个任务、应该读取哪些资料、可以调用哪些工具,以及完成以后要怎样检查和记录。
如果只有模型,得到的通常是一段针对当前对话的回答。加上项目级 Harness 后,模型才有机会沿着项目既有的规范和状态继续工作。AGENTS.md、spec/、task、Hook、workflow-state、测试命令和 journal,都属于这层工程结构。
Trellis 的价值正在这里。它把一次次对话需要依赖的项目事实放进仓库和运行流程里,再按当前任务动态提供给模型。和单纯拉长 Prompt 相比,这种方式更容易长期维护。
从文件结构看,Trellis 也没有把所有规则塞进一个说明文件,而是按生命周期把项目材料拆开。一个初始化后的项目大致会是这样,具体任务文件和平台目录会根据实际使用情况生成:
1 | 项目根目录/ |
.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。它需要同时看到:
- 当前的 workflow-state。
- 任务里的需求、设计和实施清单。
- 当前功能涉及的项目 spec。
- 仓库中的真实代码、接口和已有测试。
- 这一轮新增的具体要求。
这套上下文顺序能让模型更贴近现有项目。比如要修改一个接口,它不仅要知道“新增字段”,还要知道项目统一的错误格式、目录边界和测试要求。对于前端任务,也需要遵守已有的组件和交互约定。
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 官方文档


