手把手教你设计一个成熟的 Agent Harness
手把手设计一个成熟的 Agent Harness
讲思路和选型,不绑定具体框架。目标是推演一个问题:一个能长期跑、断线能恢复、上下文不被撑爆、出错时不撒谎的 Agent 系统该怎么搭。 写给刚上手做 Agent 的人,术语第一次出现时用一句人话解释。
前言
大家好,我是汉堡🍔。
写这篇只有一个原因:做 Notus(https://notus.hejiajun.com/) 的 Harness 时踩了一堆坑,想汇总下来分享。任务跑到一半断线、模型说完成了其实什么都没做、上下文越滚越贵最后撑爆、一个危险操作崩溃后被重放了两次——这些坑在讲 Agent 的文章里基本看不到,它们大多停在「怎么调 API、怎么接工具」。
所以本篇文章将这些坑进行汇总,帮助大家少踩一些 Agent Harness 上的坑。
术语说明
- Agent:能自己决定下一步、调用工具、把任务做完的程序。
- Harness:包在模型外面的那层调度代码,管状态、管预算、管工具、管失败。
- 上下文(context):每轮发给模型的全部内容,包括系统提示、历史消息、工具定义、工具返回结果。
- token:模型计费和限量的单位,一个汉字大致 1 个 token。
- 工具调用(tool call):模型不直接干活,它输出"我要调用某个工具,参数是这些",由你的代码执行完把结果回灌给它。
- 检查点(checkpoint):某一刻的完整可恢复快照,崩溃后从这里接着跑。
- 幂等(idempotent):同一个操作重复执行,结果和执行一次一样。
- SSE:服务端往浏览器单向推流,聊天界面那种边生成边显示的能力。
目录
- Agent 里哪部分是 Harness
- 成熟度阶梯
- 上下文的成本与上限
- 架构上的两条界线
- 核心部件
- 状态与恢复
- 失败路径的处理
- 行为验收
- 技术栈选型
- 高频坑清单
- 落地路线
1. Agent 里哪部分是 Harness
把 Agent 理解成"接了大模型的对话框",后面会一直走错路。能干活的 Agent 由四部分组成:
▼text复制代码Agent = LLM(决策) + Harness(调度与约束) + Tools(手脚) + Memory(记忆)
LLM 决定下一步做什么,它是概率性的,会犯错、会跑偏;Tools 是它接触外部世界的通道;Memory 是能带到下一轮的记录;Harness 是剩下的全部——谁发请求、谁管状态、谁卡预算、谁兜住失败。
Harness 的职责压成一句:把模型的不确定输出,变成外部世界能核对、能恢复、能追查的动作。
类比操作系统:模型是 CPU,Harness 是内核。CPU 再快,没有内存管理、没有中断处理、没有权限控制,也跑不出能用的系统。模型决定能力上限,Harness 决定这些能力能不能稳定发挥。
有一条认知要提前建立:模型聪明,不等于系统可以偷懒。恰恰因为它会自己挑工具、看错误重试、为达目标做出你没预料到的动作,Harness 才必须更严谨。它的作用是在模型越界、卡死、断线、说错话的时候,把世界拉回一致状态。
2. 成熟度阶梯
Harness 没法一步到位。成熟系统基本都沿同一条台阶爬上来,你可以用它对一下自己的位置。
| 台阶 | 你能做到什么 | 还没解决 |
|---|---|---|
| S0 无脑拼接 | 把历史消息和检索结果全塞进 prompt,能对话 | 上下文爆炸、答非所问、幻觉 |
| S1 单轮工具调用 | 模型选工具、工具返回、直接回复 | 复杂任务断在半路 |
| S2 多轮循环 | 工具结果回灌,循环到出结果 | 工具结果越滚越大,可能死循环 |
| S3 有状态、能恢复 | 会话状态 + 检查点 + 任务队列 + 断线重连 | 上下文治理、重复执行、恢复语义 |
| S4 上下文治理 | 结果外置、按需读取、每轮重新拼视图、预算控制 | 结果对不对没法验收,拦不住模型自称完成 |
| S5 契约与护栏 | 任务意图、工具裁剪、交付前核对、权限控制 | 要持续打磨,要用真实模型回归 |
大部分教程停在 S2。S3 到 S5 才是难点,后面每一节都对应这条阶梯。
3. 上下文的成本与上限
这篇只留一句结论的话:上下文有硬上限,越长越贵,而且每一轮都要重新付一次钱。
四个具体后果:
- 体积随轮数增长。 每轮都要追加一条模型输出和一条工具结果,量随"轮数 × 每轮工具数 × 单项结果大小"滚动。读一篇长文或抓一整页网页,几万 token 就没了。
- 注意力会衰减。 技术上塞得下,模型对超长上下文中间部分的关注也会下降,关键信息容易被淹掉。
- 越长越贵。 每轮都要重发整个历史,长任务是成本的放大器。
- 一次外置省的不是一轮。 省下的是后面所有轮的重发量。
大对象的处理顺序
面对文件、检索结果、网页正文、图片这类体积大的材料,成熟系统的处理顺序是固定的:
▼text复制代码外置 → 引用 → 按需读取 → 投影
- 外置:正文不进对话,脱敏压缩后写进独立存储。
- 引用:上下文里只留一个可寻址的引用,带上状态、大小、摘要、哈希。
- 按需读取:给模型一个受控的读取工具,让它自己决定读不读、读哪段、读多少。
- 投影:每次发请求前,按这一轮预算临时拼一个视图,而不是去改原始消息。
截断与投影的区别
新手的常见做法是"太长了就砍掉前面几条"。这会同时踩三个坑:砍掉的可能是模型正在依赖的事实;原始数据被破坏,出问题没法回溯;每轮砍法不同,行为没法回归测试。
正确做法是原始消息永远不动,只在发请求前拼一个临时视图。视图用完就丢,检查点才是恢复的唯一依据。可恢复和可复现,是 Harness 的两条底线。
4. 架构上的两条界线
第一条:把"决定能不能做"和"实际执行"分开。 前者管状态、权限、调度、预算;后者负责调模型、跑工具、写文件、发事件。混在一起写,后面加权限和并发时会大面积返工。
第二条:执行不要挂在 HTTP 请求上。 新手做法是用户发一个请求,服务端在请求里跑完整个循环,用 SSE 推流。后果是用户关掉页面任务就丢了,网络抖一下任务半死,没法排队也没法并发。
成熟做法是请求只负责建任务,执行交给独立的后台进程和任务队列,SSE 降级成一个随时可以断开的观察通道:
▼text复制代码请求 → 落库(session + task) → 立即返回/建立订阅 Worker → 领任务 → 跑循环 → 写事件 → 广播 订阅 → 按事件游标补齐 + 接收增量
这两条界线划清之后,断线和取消就分开了。断线只是没人在看,任务还在跑。
数据表的职责拆分
不要全堆进一张会话表:
sessions:会话状态、当前任务、租约、预算、版本号messages:用户/助手/工具消息,原始权威数据checkpoints:可恢复快照,版本化tool_results:外置结果的元数据,只存路径、哈希、大小、状态,不存正文runtime_facts:工具调用的执行事实,用于判重和判断"结果未知"run_events:进度、产物、最终结果这类语义事件,用于重建时间线usage:逐次用量,主模型、检索、重排分开记
迁移方式用加法:新表加、旧字段留着兼容,不要为了"干净"破坏性删列。
5. 核心部件
5.1 上下文装配器
它决定每次发给模型的消息数组长什么样,是最难写对的地方。
先划预算。不要用"最多 N 条消息"这种拍脑袋规则,按模型真实上下文窗口算,下面的比例是经验起始值:
▼text复制代码安全余量 = window × 10% // 固定,任何情况不借用 可用区 = window × 90% system + 工具定义 ≈ 20% 当前任务 ≈ 20% 活动工具链 ≈ 25% 动态材料 ≈ 25%
触发规则:
▼text复制代码估算输入 ≥ 硬预算 × 85% → 压缩到约 75% Provider 报 overflow → 压到约 60%,只重试一次 仍超限 → 存检查点,明确报"预算超限",不硬撑
会话累计用量再设三档:到窗口 70% 主动压缩;到 85% 停止加载非必要材料和可选工具;到 100% 暂停,等用户确认扩容。
刹车用预算,不要用"最大轮次"。轮次上限看着安全,实际会把长任务硬生生切断,逼用户点继续。预算和循环守卫这两道刹车互相独立,互不干扰。
5.2 历史怎么进上下文
历史是上下文的第一大来源,分三级:
- 近期原文:按预算选,不按条数。"最近 10 条 + 每条截 1400 字"这种写法就是长对话丢事实的根源。
- 滚动摘要:预算外的内容分批摘要,必须带来源指纹(对覆盖范围内的消息算哈希),消息被改写或删除就失效重建。摘要会丢信息,所以摘要不是权威,原文才是。摘要服务失败要明确降级,告诉模型"我记忆不完整",不要假装记得。
- 历史检索入口:给模型一个回查历史的工具,支持关键词和分页,只能读当前会话、且早于本轮用户消息的部分。
摘要本身也要限资源,防止它变成无底洞:单次摘要最多 3 次模型调用、最长 60 秒窗口、最多花掉剩余任务预算的 25%,失败就降级为可查询原文,不阻断主任务。
5.3 工具结果外置
这一步是从 S3 迈向 S4 的主要动作。常规工具的完整结果在进入上下文之前,先做三件事再落盘:
▼text复制代码脱敏 → 压缩 → 原子写 路径: <dataRoot>/tool-results/<会话>/<任务>/<uuid>.json.gz
给模型的不是正文,而是一份回执:
▼json复制代码{ "tool": "search_notes", "status": "ok", "summary": "返回 3 篇候选,最高相关度 0.83", "result_ref": "tool-result://8f3c...", "sha256": "sha256:...", "original_bytes": 184320, "stored_bytes": 41250, "fields": ["title", "path", "score"], "read_hint": "可用 json_pointer / query / offset 读取" }
配额给死:单项未压缩结果不超过 32 MiB,单会话压缩后占用不超过 512 MiB,超了就明确报错,不要静默截断。
外置失败时记住一条:执行状态和存储状态是两回事。工具真的执行成功了、只是归档失败,就如实标记"载荷不可用",并停掉依赖正文的后续步骤,但不要把这个已经成功的外部调用改写成失败。
图片和附件同理:进上下文的只有一个引用,发送前由投影器加载成多模态输入,base64 不进正文、事件流和检查点。
5.4 读取工具的接口设计
外置之后,模型怎么把内容读回来,这里很容易踩坑:不要直接把服务器文件路径交给模型。路径会泄漏内部结构,模型可能读到它无权访问的内容,而且很多宿主根本没有 shell 让它去 cat。
正确做法是自己实现一个受控读取工具,只接受会话内有权限的不透明引用:
▼text复制代码read_result(ref, mode) ├─ json_pointer: "/items/0/content" ├─ query: 关键词窗口,命中段落返回 └─ offset+size: 第 8KB 起读 4KB 默认返回 8 KiB,硬上限 16 KiB
再配一个索引工具,让模型能分页列出本会话有哪些结果可读。
这一步是自研 Harness 和套壳 Harness 的分界线。套壳方案给模型一个 shell;自研方案给它一条带权限、带分页、带配额的读取通道。前者在沙箱里成立,后者在不允许执行任意代码的宿主(笔记应用、办公套件、SaaS 后台)里是唯一选择。
5.5 压缩的顺序与约束
压缩不是砍前面的,分两级、有顺序:
▼text复制代码1) 先回收大读取窗口 优先回收"较早 且 估算超过阈值(如 2048 token)"的读取片段,预算满足就停 最近 4 条消息、错误结果、小型目录暂不回收 2) 还超预算,再按完整的"调用 + 结果"分组压缩 一条 tool_call 和它的结果必须成对处理,压缩时保留读取参数(offset/limit)
三条硬约束缺一不可:保持调用和结果配对(拆散了下一轮请求会被直接拒绝);不改原始消息;不自动重放工具,压缩只是让模型看不见,不是让它重新执行。
有个典型坑:把"只保留最近 4 条"写成无条件逻辑,模型刚读过的内容被过早收走,只好反复重读同一个大文件,token 反而更费。
5.6 工具运行时
工具不只是函数,它是一份契约:
▼text复制代码Tool = { name, description, input_schema, // JSON Schema,执行前必须校验 handler, permission, // 是否需要用户确认 idempotency: read_only | idempotent | unknown_side_effect, result_policy: { maxBytes, redact, archive } }
参数要在执行前校验(JS 用 Ajv 或 zod,Python 用 pydantic)。要区分"工具不存在"和"参数非法":前者是授权或投影问题,后者是模型问题,混在一起模型会误以为这个能力不可用。校验失败要把错误回灌给模型让它改,不要直接终止。
按副作用把工具分三类:
| 类别 | 例子 | 崩溃重启后 |
|---|---|---|
| read_only | 读文件、检索 | 可以安全重放 |
| idempotent | 按 ID 覆盖写 | 可以恢复 |
| unknown_side_effect | 发邮件、下单、装软件、调外部 MCP | 绝不自动重放 |
第三类在调用前先写一条 prepared 事实。崩溃重启后发现"有 prepared、没有终态",就标记成 outcome_unknown(结果未知),把错误和已有引用交回模型,让它自己判断,而不是替它决定重来一次。在"可能重复执行一次危险操作"和"如实告诉用户结果未知"之间,选后者。
工具来自多个来源(内置 + 扩展 + 外部 MCP)时,不要一次把 schema 全塞进上下文:启动只给一份摘要目录,模型需要时用 list_tools 分页发现,下一轮再给完整 schema。
结果治理跟装配器配合:执行期先做一次大小裁剪(读文件 256KB、网页 64KB 就截断并附摘要);脱敏只在归档前做一次,按需读取时不要再脱敏,否则会把路径、哈希这类合法内容也破坏掉;失败结果也要落盘,错误正文同样值得被按需读取。
5.7 循环终止条件
循环骨架很简单,难的是终止条件:
▼text复制代码while (true) { resp = callLLM(messages, tools) if (!resp.tool_calls && stopReason in [end_turn, stop]) break // 正常收尾 if (invalidToolInput(resp)) { pushError(resp); continue } // 让模型改 results = dispatchTools(resp) messages.push(assistant(resp), toolResults(results)) }
四类终止条件互相独立:
- 正常完成:没有工具调用,加上明确的结束原因。
- 交付前核对:结束后按任务要求对一遍,需要联网的有没有证据,需要写文件的有没有产物。最多给一次修正机会,仍不足就如实报告未完成。
- 循环守卫:相邻、同工具、同参数、同结果连续 N 次,把已有结果交回模型并提示;同错误码连续 M 次,交回模型判断;连续空响应判为无进展。
- 预算耗尽:走 5.1 的闸门。
守卫负责拦住明显的原地打转,不要把它写成"重复调用直接判失败",那会误伤正常的"不同 URL、不同分页"读取。
6. 状态与恢复
能不能把 S2 抬到 S3,基本决定了 Agent 产品能不能用。工具谁都会接,状态很少有人做对。
会话状态显式建模,不要用一堆布尔标志位:
▼text复制代码created → running → waiting_interaction → queued_resume → running → waiting_limit // 等用户确认扩容 → waiting_retry // 临时错误重试耗尽 → waiting_recovery // 需要用户处理:额度、密钥、权限 → completed | failed | cancelled
每个状态变化都要幂等:同一份用户回答重复提交,只返回同一个恢复任务。每个状态都要能回答"现在能不能继续、能不能取消、能不能输入"。
检查点两阶段提交,不要"先删旧再写新":
▼text复制代码1) 读当前 active 检查点(不动) 2) 写入新的检查点内容 3) 事务内把新记录置为 active,更新会话指针 4) 成功之后,才把旧的置为 superseded
在步骤 3 之前崩溃,旧检查点照样能救回来。
租约(lease):执行前拿一个带时限的租约,比如 90 秒,执行中定期续租,比如每 20 秒,状态更新时同时匹配当前执行者 ID 和版本号。进程崩溃后租约过期,允许新执行者接管。没有租约,会出现两个 Worker 同时跑同一个会话,把文件写两遍。
交互票据:需要用户回答、确认或恢复任务时,用短期、一次性、带签名的票据。票据只走请求体或请求头,不进 URL、日志和消息正文,校验时匹配 owner,用完即废,防止重放。
事件推送:事件先写数据库再广播,这样断线重连才能按游标补齐。事件分语义通道,不要只发原始 token:progress 是短进展,artifact 是交互、预览、文件状态和待确认项,final 只出现一次,落成最终消息并带累计用量。订阅用游标,重连时从"我收到哪了"继续,避免重放旧的 final 导致消息重复。加心跳,比如每 15 秒一行注释,防止空闲连接被代理掐断。进程内的事件总线要挂在全局单例上,否则多个模块会各发各的。
断线不等于取消:用户显式点取消才算 cancelled;浏览器断线就存检查点,任务保留为 queued_resume,回来接着跑。恢复的规则是复用检查点和已有的工具结果,不重放已完成的工具,按"下一个待分发的工具索引"继续,配合执行事实表去重。
7. 失败路径的处理
成熟与否,在失败路径上最明显。
错误先分类,不要都当失败:
▼text复制代码retryable 网络抖动、429、部分 5xx → 等待 + 有界重试(如 30s × 最多 5 次) action_required 额度、密钥、权限、模型不可用 → 等用户处理后可继续 fatal 请求结构非法、安全或上下文无法恢复 → 终止
重试要绑定取消信号:用户停止任务时,正在等待的重试定时器立刻取消,不要"再发一次试试"。
失败结果同样落盘,回执带原因摘要和错误字段位置。让模型读错误、改参数、换个方式继续,不要替它判断"这工具坏了"。多数"工具不可用"的假象是参数写错了。
给用户看的错误要克制:一张错误卡片加一个继续入口,不透传 Provider 的 HTML 错误页或原始响应正文。内部诊断要尽量全,包括 HTTP 状态、错误码、请求窗口、预算数字、消息角色统计、工具调用与结果配对统计、耗时,这些用来排障,不能省。一份好的错误信息能把排障时间从半天压到几分钟。
8. 行为验收
到 S5,问题不再是能不能做,而是行为能不能被验收。
提示词要模块化、带版本。 几千行的巨型 system prompt 没法维护,改成注册表驱动:政策、工作区规则、写作规则、检索规则、资源规则、交互规则、输出规则分模块,每个模块带模块 ID、规则 ID、优先级、适用条件、token 上限和对应的测试用例。渲染阶段做三件事:拒绝重复的规则 ID、拒绝显式冲突的规则、校验材料配额,并记录提示词版本,让每次请求可追溯。
不可信材料要降权。 检索结果、网页、外部 API 返回、用户上传的文档都是数据,不是指令。用统一信封包起来,带上来源类型、来源 ID、信任等级、摘要哈希。信封内容永远不能覆盖系统策略,也不能扩大工具权限。用户自己维护的长期记忆可以是高信任等级,但依然低于策略和权限。
把判断从模型手里收回。 早期做法是让一次模型调用同时判意图、选材料、选工具、判完成,结果是行为没法验收,你想测的每条路径都挤在同一次概率输出里。成熟做法是拆成可持久化的契约:这一轮的任务意图由 Harness 采集并记下来(意图、目标对象、用户更正、待核实问题、证据要求);每轮重新挑材料,不沿用任务开头那个大上下文;按意图编译这轮该给的工具;交付前按契约核对结果。
提示词写法上的六条经验:顺序判断(第一步→第二步)比"综合考虑 ABC"对弱模型友好;是非题比开放题准;给映射表比描述规则省事;几个具体例子胜过一段长规则;显式写兜底(不确定时提问)比让模型自己想兜底安全;划边界(永远不要声称做了没做的事)比列路径有效。
外部副作用靠声明,不靠推导。 不要根据工具名字猜它有没有副作用,让工具自己声明。未知副作用的工具默认串行、默认不可自动重放。
9. 技术栈选型
Harness 对技术栈要求不高,对可靠性原语要求高。
语言:Node.js / TypeScript 生态最全,前端同语言,流式成熟;Python 的 AI 生态最好,数据处理强;纯 JavaScript 也能做,但要靠 Schema 补上类型约束。
存储与检索:
| 需求 | 单机 / 本地优先 | 服务端 / 多租户 |
|---|---|---|
| 关系 + 状态 | SQLite(WAL 模式) | PostgreSQL |
| 全文检索 | SQLite FTS5 | Postgres tsvector / Elastic |
| 向量检索 | sqlite-vec | pgvector / Qdrant |
| 结果文件 | 本地文件系统(原子写) | S3 兼容对象存储 |
| 缓存 / 队列 | DB-backed queue | Redis(BullMQ 等) |
会话、检查点、事件这些最关键的数据优先放关系库,并保证写事件和写状态能在同一个事务里完成。
传输:SSE 优先,单向、浏览器原生、经得起代理;需要双向再上 WebSocket。无论哪种,事件先落库再广播,重连靠游标。
队列与并发:起步用数据库任务表加轮询 Worker,简单、可恢复、够用;复杂编排再上 Temporal 这类状态机引擎。并发模型用同会话 FIFO、跨会话并行,既保序又不互相堵。
可观测性:结构化日志,加上每次请求的诊断字段;密钥、凭据、带登录信息的 URL 一律脱敏后再进日志、事件和消息;提供一个只读的"任务活动"查询,让模型和用户都能回看真实执行记录。
沙箱:宿主允许执行代码时,轻量用 Docker + gVisor,强隔离用 Firecracker microVM。给了 shell 反而要更严,因为模型能自己 cat 的时候,权限和配额更容易被绕过。
10. 高频坑清单
| 现象 | 原因 | 对策 |
|---|---|---|
| 长对话丢早期事实 | 用"最近 N 条"裁剪 | 预算化历史 + 摘要 + 历史检索 |
| 压缩后仍然溢出 | 只压缩成功的工具结果,没救到长正文 | 先算预算,再回收大读取窗口 |
| 模型反复重读同一文件 | 读取窗口被无条件驱逐 | 预算够就保留,按大小阈值回收 |
| 工具结果撑爆上下文 | 正文直接进上下文 | 外置 + 引用 + 按需读取 |
| 请求报 prefill 或 reasoning_content 错误 | 模型协议差异没归一化 | 统一适配层,修消息形状 |
| 断线后任务丢失 | 执行挂在 HTTP 请求上 | 队列 + Worker,SSE 只观察 |
| 消息重复出现 | 恢复时从 0 重放旧 final | 事件游标去重,final 只写一次 |
| 同一会话被跑两遍 | 没有租约或版本号 | lease + 版本号条件更新 |
| 危险操作重复执行 | 崩溃后盲目重放 | 幂等分类,结果未知不重放 |
| 模型谎报完成 | 没有交付前核对 | 按契约核对 + 一次纠正 |
| 脱敏把合法内容破坏 | 归档和读取反复脱敏 | 只在归档前脱敏一次 |
11. 落地路线
按台阶来,每一级都要求可恢复、可回归:
▼text复制代码里程碑 1(S1→S2)跑通多轮循环 验收:能完成一个需要 3 次以上工具调用的任务 里程碑 2(S3-a)执行与请求解耦 验收:断线、刷新、重启进程后任务都能继续,SSE 断线不取消任务 里程碑 3(S3-b)状态与恢复 验收:检查点两阶段提交;并发被租约挡住;重复回答只产生一个恢复任务 里程碑 4(S4-a)上下文预算 验收:构造超长输入,能在触发线压缩,overflow 只重试一次,不越界 里程碑 5(S4-b)结果外置与按需读取 验收:大结果只给引用;模型能用读取工具取回指定片段;配额生效 里程碑 6(S4-c)投影与压缩 验收:压缩保持调用与结果配对;原始检查点不被改写;已读材料不无故丢失 里程碑 7(S5)契约与护栏 验收:任务意图可持久化;工具按意图裁剪;交付前核对能拦下只说不动; 未知副作用不重放;提示词有版本和冲突检测 里程碑 8 可观测与真实模型回归 验收:错误可诊断、密钥不泄漏;真实模型 + 真实工具 + 多端跑通
验收上有一条底线:不要拿"接口存在"当"功能可用",也不要拿"静态测试通过"当"用户路径通过"。测试要覆盖故障注入,而不只是正常路径:
- 检查点在模型失败、数据库失败、进程被杀这三种时刻崩溃,能否恢复;
- 取消、超时、心跳、租约冲突;
- 超长消息、超大工具结果、overflow 单次重试;
- 非幂等调用发出后中断,是否被正确标为结果未知且没有被重放;
- 密钥和凭据是否出现在日志、事件、消息元数据里(应为 0)。
结语
把这些取舍连起来,方向是一致的:能外置的就外置,能引用的就引用,能恢复的不重放,能算预算的别数轮数,能核对的别信模型的自我汇报。
模型会越来越强,但 Harness 里关于状态、权限、恢复的这些工作不会因为模型变强而消失。模型给的是概率性的输出,外部世界需要的是确定、可恢复、可追查的结果。
我的项目
Notus:本地优先的 AI Agent 笔记软件 喵喵搜索:自托管多引擎网络搜索和远程MCP服务,覆盖必应、百度、微信公众号、DuckDuckGo、CSDN、GitHub 等多个搜索源,支持微信公众号、CSDN、掘金、GitHub README、Linux Do、小红书等文章内容抓取。 Project Vibe Spec:用于 Vibe Coding 项目的协作与交付治理:把用户确认、项目事实、设计决策、实现、验证和进度维持在同一条可追溯链路上,同时避免把所有上下文塞进一个巨大的 AGENTS.md。
