编程导航GitHub话题讨论

GitHub

980 参与
分享

快来分享你的内容吧~

点击登录,快来和大家讨论吧~
表情
图片
话题
打卡
综合
交流
文章
问答

IP 数据库体积缩小 31%,关键竟然是合并重复子树

> 根据 IPinfo 的文章 [How We Made Our MMDB Files a Third Smaller Without Changing a Byte of Data](https://ipinfo.io/blog/how-made-mmdb-files-smaller) 整理。原文作者:Tiago Martins、Silvano Cerza。 MMDB 是一种用于查询 IP 地址的二进制格式。IPinfo 发现,文件的搜索树中有许多重复子树,只要把它们合并,就能在不改变查询结果的情况下缩小文件。 这一方法让 IPinfo 的 89 个生产数据库总大小从 91.2 GB 降至 62.8 GB,减少了约 31%;其中缩减幅度最大的数据库达到了 80%。优化后的文件仍是标准 MMDB,无需解压,也不用更换读取库。并且开源啦! [mmdbshrink](https://github.com/ipinfo/mmdbctl/tree/master/mmdbshrink) ## MMDB 文件的结构 MMDB 文件主要由**搜索树**和**数据段**组成。 搜索树是一棵按 IP 地址比特位构建的二叉前缀树。查询从根节点开始,逐位读取 IP 地址:遇到 0 向左,遇到 1 向右,直到抵达叶节点。叶节点指向数据段中的记录,例如国家、城市、ASN 和隐私标志。 ![MMDB 文件的结构](https://pic.code-nav.cn/post_picture/1608460212774109186/KyM2FuVLmmrcWZc0.webp) 数据段中的重复记录只需存储一次,多个叶节点可以通过指针引用它。常见的 MMDB 写入器已经做了这类去重。 搜索树却可能包含大量重复子树。传统写入器会写出遍历到的每个节点,即使已经写过结构相同的节点,也会再存一份。 ## 重复子树从哪里来 IP 数据中有大量重复值。例如,隐私数据库可能将少数几种判定结果应用到数百万个地址范围;地理位置数据库中的同一城市记录也可能对应许多不同前缀。 如果两个前缀下的子树结构相同,对应位置又指向相同的数据记录或空结果,那么它们就是重复子树。写入器虽然会把它们分别存储,查询时却不需要保留这些独立副本。 ![标准写入器与哈希驻留后的 MMDB 搜索树对比](https://pic.code-nav.cn/post_picture/1608460212774109186/3ISwoMYRdJuZPmox.webp) ## 用哈希驻留共享子树 **哈希驻留**可以用来复用结构相同的对象。它用一张哈希表记录已有节点:创建新节点前,先检查是否存在结构相同的节点;如果存在,就直接复用。 对 MMDB 搜索树,可以从叶节点向根节点自底向上处理: 1. 先处理子节点,再处理父节点。 2. 将子节点引用替换为对应的规范节点标识;数据记录和空结果的引用保留原有含义。 3. 将左右两个子节点引用组成的**有序对**作为哈希表的键。如果已有完全相同的有序对,就复用对应节点;否则创建并登记新节点。查表时还要比较完整的有序对,避免因哈希碰撞错误合并节点。 4. 写出去重后的节点,保留根节点编号为 0,重新编号节点引用,并更新 `node_count` 和编码后的数据引用。 处理后,搜索树变成了**有向无环图**:多个父节点可以指向同一棵子树,文件中只需保存一份。 MMDB 节点本来就能指向其他节点、数据记录或空结果;[格式规范](https://maxmind.github.io/MaxMind-DB/#binary-search-tree-section)也描述了 IPv4 别名中的节点共享。因此,这种优化不需要修改文件格式。对任意 IP 地址,查询仍会经过相同的分支选择,最终到达相同的记录或空结果。 相关背景可参考 [Filliâtre 和 Conchon 的哈希驻留论文](https://doi.org/10.1145/1159876.1159880),以及 [Bryant 的二元决策图论文](https://doi.org/10.1109/TC.1986.1676819)。 ## 实际效果 不同数据库的缩减幅度取决于搜索树中重复子树的数量。以下是 IPinfo 给出的部分生产数据: | 数据库 | 优化前 | 优化后 | 缩减幅度 | | --------------------- | ----------: | ----------: | -------: | | `ipinfo_privacy` | 789 MB | 155 MB | 80% | | `ipinfo_location` | 687 MB | 316 MB | 54% | | `ipinfo_core` | 1.5 GB | 728 MB | 53% | | `ipinfo_lite` | 38 MB | 25 MB | 34% | | `ipinfo_plus` | 5.6 GB | 3.8 GB | 32% | | `resproxy` | 3.5 GB | 2.6 GB | 27% | | **总计:89 个数据库** | **91.2 GB** | **62.8 GB** | **31%** | 在原文的 `ipinfo_lite` 示例中,文件从 37.76 MB 降至 25.08 MB,节点数从 3,790,310 降至 2,205,358。搜索树从 30.32 MB 缩至 17.64 MB,数据段则保持 7.44 MB 不变:节省的空间全部来自搜索树。 原文报告的一百万次查询基准中,单次查询耗时从 797 ns 降至 757 ns;映射文件的缓存大小从 36.02 MiB 降至 23.92 MiB。实际进程常驻内存则从 29.34 MiB 降至 27.58 MiB。文件更小有利于降低存储和传输成本,内存收益则取决于实际访问和映射情况。 ## 如何验证查询结果不变 优化索引结构的前提是不能改变查询结果。IPinfo 使用了以下检查: - **双向枚举前缀:** 在优化后的文件中查询原文件的每个前缀,再反向执行。`ipinfo_lite` 涉及 3,783,922 个前缀、7,567,844 次查询,结果全部一致。 - **随机地址查询:** 分别检查一百万个随机 IPv4 和 IPv6 地址,没有发现不匹配。 - **格式和差异检查:** `mmdbctl verify` 验证文件有效;`mmdbctl diff` 显示没有子网或记录被修改;MaxMind 的 `mmdbverify` 也通过验证。 - **兼容性与性能测试:** 使用 12 个 MMDB 读取器 SDK 测试,并比较优化前后的查询延迟与内存表现。 其中,双向前缀枚举比单独依赖随机地址采样更系统地检查了两个文件的映射关系。

新版 Cursor 用不了 cursor-byok?修好啦!

分享一个最近修复并开源的项目:**cursor-byok 社区维护版**。 GitHub:https://github.com/leehainuo/cursor-byok 原项目:https://github.com/leookun/cursor-byok cursor-byok 是一个本地 Cursor 模型网关,允许开发者配置自己的 OpenAI / Anthropic 兼容模型 API,并通过本地协议适配继续使用 Cursor 的 Agent 工作流。 由于原项目目前已归档,随着 Cursor 客户端更新,旧版出现了 Agent 通信协议不兼容的问题。 最近我基于原项目进行了修复,并创建了一个独立维护的开源版本。 **本次主要修复:** 新版 Cursor 的 Agent 通信路径出现变化,可能使用与原网关预期不同的请求消息。维护版本调整了相关功能开关及协议兼容处理,使请求继续使用现有网关能够处理的 AgentRunRequest 流程。 项目仍然保留原有的模型接入和 Agent 工具协调架构,后续会持续关注 Cursor 版本变化及兼容性问题。 需要说明的是,这不是 Cursor 官方项目,也不能保证所有客户端版本、模型和功能均已验证。 欢迎遇到类似问题的开发者查看源码、测试、反馈 Issue 或提交 PR。 感谢原作者 leookun 及所有原项目贡献者。 如果对大家有帮助,欢迎 Star 支持!

做了三年 ProseMirror 后,我用四个月自研排版引擎实现 WPS / Google Docs 替代品,整套框架 MIT 开源

先别急着划走,这不是概念图,也不是拼出来的截图。 打开 [https://quirekit.com/demo/classic-editor](https://quirekit.com/demo/classic-editor/),首屏就是一个真实的编辑器:一页一页的纸,打字、换行、表格跨页、切换 Canvas / SVG 渲染,全都是实时的。 ![02-classic-paged.png](https://pic.code-nav.cn/post_picture/1697067765011161089/tJ3spOnH5nU0D63Z.webp) *演示里的 DOCX 导出、页眉页脚等是商业 Word 组件,编辑器框架、排版引擎和编辑器本身是开源的。* 这是我过去四个月在做的 **QuireKit**。它不是某一个编辑器,而是**一整套编辑器生态**: - **最底层**是两套编辑器框架,写法都是 Tiptap 风格:一套基于 ProseMirror,API 兼容 Tiptap;一套基于 CodeMirror,专门做 Markdown; - **中间**是自研的排版引擎、Canvas / SVG / HTML 等多种视图,以及一套统一的 UI 组件库; - **最上层**是能直接用的编辑器:分页文档、块编辑器、Markdown 编辑器,还有公式、思维导图、白板、流程图这些周边编辑器。 每一层都可以单独拿去用,也可以整套组合起来。一句话说它能干嘛:**用它,你可以在自己的产品里搭一个“自己的 WPS 在线版 / Google Docs”。** ## 先说做出了什么 ### 1. 浏览器里真正分页的文档编辑器 做过在线文档的都知道,浏览器只会排一条无限长的页面。想要“一页一页的纸”,常见做法是排完再量高度、手动切页,内容一改就全乱。 QuireKit 的分页编辑器不靠 DOM 排版:**断行、分页、光标、选区全部自己算,再画到 Canvas 或 SVG 上**。屏幕上看到的分页,就是打印出来的分页。 - 表格可以跨页,表头自动重复 - 公式、流程图、思维导图、白板直接放进页面里一起排版 - 暗黑模式、中英文界面切换 - 长文档只绘制看得见的页,滚动不卡 操作面板也不是摆设。我按 Word 的功能区逐项对过一遍:开始、插入、页面、引用、审阅、视图,加上右键菜单、格式设置面板、表格和图片的上下文工具,**已经有 350+ 功能能直接在界面上点出来用**,还在继续补。 **DOCX 往返也打通了**:Word、WPS 写的文档导进来,可以接着改,再导出成 DOCX,拿回 Word 里继续用。 **PDF 和 OFD 导出也是自研的**:不走浏览器打印,直接复用分页排版画出来的那一份绘制指令,屏幕上一页什么样,导出的 PDF / OFD 就是什么样。OFD 是国产版式文件格式,政务、公文场景用得多。 DOCX 往返和 PDF / OFD 导出属于商业的 Word 组件,开发和评估免费。 ### 2. 块编辑器 不需要分页的场景,比如笔记、知识库,用块编辑器:拖拽、斜杠菜单、表格、目录都有。 ![05-block-editor.png](https://pic.code-nav.cn/post_picture/1697067765011161089/B8bGBgJqmSSo07Iv.webp) 它解决了一个用过 contenteditable 的人都踩过的坑:**你选中一段字,去工具栏输个链接,选区就没了。** 在 QuireKit 里,光标和选区是自己画的,焦点跑到输入框,选区照样在。 ![04-view-html-selection.png](https://pic.code-nav.cn/post_picture/1697067765011161089/5Z24YbFSMAuK4qGj.webp) ### 3. Markdown 编辑器:原文一个字不丢 MD Editor 有四种视图:文档、实时预览、源码、阅读。前三种都能直接编辑,**改的都是同一份 Markdown 原文**,中间没有转成 JSON 再转回来,所以保存时你没碰过的行,一个字符都不会变。 ![06-md-editor-views.png](https://pic.code-nav.cn/post_picture/1697067765011161089/3PAzzuKCiQ2j6M2B.webp) 大文档也不怕。我给 Markdown 解析器加了一个“复用证明”,编辑时直接跳过没变的部分。1 MiB 的文档连续输入 20 次,实测: | 工作量 | 优化前 | 优化后 | | --- | --- | --- | | 语法树访问 | 107,760 | 1,380(**少了 98.7%**) | | 语法树比较 | 153,940 | 660(**少了 99.6%**) | ### 4. 一整套周边编辑器,单独也能用 公式、思维导图、白板、文本绘图(Mermaid / PlantUML / Graphviz)、流程图、图表、多维表格,每一个都是独立的包,不用整个编辑器也能单独装。 ![07-editors.png](https://pic.code-nav.cn/post_picture/1697067765011161089/SMAI2QQY9mK7LJp5.webp) ### 5. 自己设计的 UI 组件库 编辑器的操作界面比一般后台复杂得多:字体菜单、颜色选择、表格右键菜单、格式设置对话框、缩放、快捷键提示……为了让分页文档、块编辑器、Markdown 编辑器和这些周边编辑器用起来是同一种手感,我单独设计了一套 UI 组件库(Vue + UnoCSS),颜色、间距、字号都由同一套设计令牌管理,亮色和暗黑两套主题一键切换。 上面截图里所有的按钮、菜单和面板,都来自这套组件库,它同样是 MIT 开源的,可以单独用在你自己的项目里:https://design.quirekit.com ## 再说说最难的一块:中文输入法 Canvas 上没有输入框,**中文输入法是最难啃的骨头**。拼音还没确认的时候,字要先显示出来、把后面的内容往后推,但又不能进撤销历史、不能同步给协作者。 我的做法是:输入法组合期间,用一份“临时文档状态”去排版和绘制,等你按下确认才真正写进文档。 ![03-ime-flow.png](https://pic.code-nav.cn/post_picture/1697067765011161089/XKzBRI5szD8j76mC.webp) 浏览器在输入法上的坑非常多,事件数据为空、组合结束事件不来、确认后重复插入……这套处理我单独做成了 `@quirekit/ime` 包,其他 Canvas 编辑器也能直接拿去用。 ## 底层:两套 Tiptap 风格的编辑器框架 ![01-architecture.png](https://pic.code-nav.cn/post_picture/1697067765011161089/8QaRzzpIKnJJ8O06.webp) - **ProseMirror 线:`@quirekit/core`**。保留 ProseMirror 的文档模型、命令、插件、协同,API 兼容 Tiptap,目标是迁移一个 Tiptap 扩展时,主要只需把 `@tiptap/core` 的导入换成 `@quirekit/core`。不同的是视图层:除了原生的 DOM 视图,还能挂自己排版的 Canvas / SVG 视图(分页文档用的就是它),以及自己管光标和选区的 HTML 视图(块编辑器用的就是它)。 - **CodeMirror 线:MSFE**。基于 CodeMirror 和 Lezer,Markdown 原文就是文档,扩展和命令的写法也是 Tiptap 风格。MD Editor 构建在它上面。 两套框架的成品共用一套 UI 组件库,所以分页文档、块编辑器、Markdown 编辑器看起来是一家人。熟悉 Tiptap 的话,两条线都能很快上手。 ## 能用吗?怎么用? 几行代码就能把 Markdown 编辑器放进 Vue 项目: ```bash npm install @quirekit/md-editor@alpha ``` ```vue <script setup lang="ts"> import { MdEditor } from '@quirekit/md-editor/vue' import '@quirekit/md-editor/styles.css' </script> <template> <MdEditor markdown="# Hello" label="Document" /> </template> ``` 块编辑器和分页编辑器分别是 `@quirekit/block-editor`、`@quirekit/classic-editor`。 **整套生态里开源的部分**,MIT 许可,闭源商用也可以:两套编辑器框架、Canvas / SVG 排版引擎和各种视图、UI 组件库、分页 / 块 / Markdown 三个编辑器,以及公式、导图、白板这些周边编辑器。Word 兼容能力(DOCX 往返、PDF/OFD 导出、审阅等)是单独的商业组件,开发和评估免费,不用它们也能完整使用编辑器。 目前是 alpha,API 还会变,欢迎来试、来挑刺: - 在线体验:https://quirekit.com - 文档:https://docs.quirekit.com - GitHub:https://github.com/quirekit/quirekit 觉得有意思的话,点个 Star 就是对我最大的支持。

DiskScope:找出是谁在悄悄吃掉你的硬盘

> 一个 Windows 磁盘分析 + 清理工具。扫 16 万个文件只要 1.7 秒, 并且会告诉你**谁在长**, 而不只是**谁大**。 --- ## 起因 C 盘又满了。 每次都得开个现成的工具, 然后等它慢慢扫, 看进度条慢慢转, 扫完再自己对着几百兆的 `node_modules` 猜哪些能删。 这些工具做的是**快照**: 告诉你此刻谁占地方。但我真正想知道的是另一件事 —— > **这个目录是什么时候开始变大的? 是不是某个东西在偷偷长?** Steam 的 workshop 素材会自动重新下载并换目录 ID, 某些项目隔三差五 `npm install` 一次, 缓存目录悄悄膨胀到几十 GB —— 这些在静态快照里完全看不出来。 所以我写了 **DiskScope**,全程vibecoding将想法变成现实。 ![image.png](https://pic.code-nav.cn/post_picture/1630476292816121857/6hBXqZn0MEFUnzme.webp) ![image.png](https://pic.code-nav.cn/post_picture/1630476292816121857/bNBu7ZO6o1pAuXWF.webp) ![image.png](https://pic.code-nav.cn/post_picture/1630476292816121857/Rul4bUZwBQrMGWZV.webp) ![image.png](https://pic.code-nav.cn/post_picture/1630476292816121857/96KCld3U3bO46XGE.webp) --- ## 四个页面 ### 1. 体积分布 Squarified treemap, 点任意方块下钻, 配可展开的层级明细。目标是**一眼看出空间去哪了**。 ### 2. 时间轴(核心) 最近 30 天**每天**写入了多少数据, 堆叠柱状图, 外加一张「谁在悄悄变大」排行榜。 这是这个工具和别的磁盘分析器最不一样的地方: ``` 今天写入 578 MB │ 最近 7 天 1.8 GB │ 最近 30 天 5.4 GB │ 30 天以上没动过 142 GB ``` 一眼就能分清: **上面那 5.4 GB 是活的东西, 下面那 142 GB 是死的**。后者才是清理的目标。 ### 3. 清理 53 条规则, 分三级: | 级别 | 数量 | 含义 | 例子 | |-----|---|---|---| | 🟢 可重建 | 39 | 删了会自动重新生成, 默认勾选 | `node_modules` `target` `dist` `__pycache__` | | 🟡 需确认 | 12 | 要重新下载, 或会丢历史记录, 默认不勾 | `.venv` `Pods` `*.log` `.idea` | | 🔴 勿删 | 2 | 勾选框**直接禁用** | `.git` `*.tfstate` | 配套 **dry-run 预演**: 先看清要动什么, 再决定删不删。 ### 4. 大文件 面向**普通文件夹**(照片 / 视频 / 安装包 / 归档)。构建垃圾规则匹配不到这些, 所以单独追踪: Top 200 大文件 + 按扩展名汇总。 --- ## 技术上花了力气的地方 ### 16 个 worker 线程, 单线程的 5.5 倍 第一版用 `fs.promises.stat` + 异步池, 16 万文件要 8.4 秒。瓶颈不是 Promise 开销, 是 **stat 系统调用本身**。 改成 worker 线程池后, 主进程只管派活, worker 各扫各的: | worker 数 | 文件/秒 | 相对单线程 | |---|---|---| | 1 | 20,844 | 1.0x | | 4 | 63,343 | 3.0x | | 8 | 90,923 | 4.4x | | 16 | **114,088** | **5.5x** | 实际效果: 扫完 161,207 个文件 / 70 GB, **1.7 秒**。 ### 31 格直方图, 任意层级 O(1) 回答「最近长了多少」 每个目录存一个 31 格的直方图(第 0 格 = 今天, 最后一格 = 30 天前的冷数据), 自底向上**滚动相加**: ```js // 父目录的直方图 = 自己的 + 所有子目录的 for (let i = 0; i < NDAYS; i++) daily[i] += child.daily[i]; ``` 于是任意层级都能 O(1) 回答"这里最近 7 天长了多少", 不需要重新遍历子树。 ### 有界小顶堆, 内存恒定 要记住最大的 200 个文件, 但不能随扫描量涨内存。堆顶始终是 Top-N 里最小的那个, 新文件比它还小就直接丢弃: ```js push(v) { if (a.length < cap) { a.push(v); this._up(a.length - 1); } else if (v[0] > a[0][0]) { a[0] = v; this._down(0); } // 堆顶换掉 } ``` 扫 5 万个文件和扫 500 万个文件, 内存都是 200 条。 --- ## 踩过的坑(这部分可能比功能更有用) ### 1. Electron 的 asar 补丁会悄悄改掉你的统计结果 Electron 会把「路径最后一个扩展名是 asar 的文件」当成 asar 包来解析: - 合法的 asar → `statSync` 返回 **0 字节**(把包根当虚拟目录) - 损坏的 asar → 直接抛 `Invalid package` 结果就是**所有 .asar 文件的大小都没被统计, 磁盘总量一直偏小**, 而且 stderr 刷一片错误。 绕法是加 NTFS 备用数据流后缀 `::$DATA`(指向文件本体), 路径最后一个扩展名变成 `DATA`, Electron 就不会去解析 asar 了: ```js function statFile(p) { if (/\.asar$/i.test(p)) { try { return fs.statSync(p + "::$DATA"); } catch { /* 退回普通方式 */ } } return fs.statSync(p); } ``` ### 2. `D:\` 自带尾随分隔符, 拼路径会多一个反斜杠 ```js path.resolve("D:\\") // "D:\\" ← 带尾随分隔符 dirPath + "\\" + fn // "D:\\software" ← 双反斜杠 path.join(dirPath, fn) // "D:\software" ✓ ``` Windows 内部会容忍双斜杠, 但**路径字符串比较会失效** —— 我那个「文件被挪走了, 帮你找回来」的功能就是这么失灵的。改用 `path.join` 一劳永逸。 ### 3. 悬停提示会残留在页面上 提示框原本是每次 hover 新建一个 div 挂在 `document.body` 上, 引用存在**柱子元素**里。点击柱子下钻 → 图表整个重建 → 旧元素被丢弃 → `mouseleave` 永远不触发 → 提示框永久留在屏幕上。 修法是改成**全局单个复用的节点**, 从根上消除这类孤儿节点。 ### 4. 文件会「被挪到隔壁去」 Steam 重新下载 workshop 素材时会换一个新的目录 ID, 文件名却不变: ``` 扫描时: ...\431960\344551134\CSCT-013.mp4 现在: ...\431960\3445511134\CSCT-013.mp4 ← 多了一个 1 ``` 于是点路径时文件「不存在了」。现在的处理是分三档: 1. 目标在 → 正常定位 2. **被挪到隔壁去了** → 按文件名在上一级的其他子目录里找一遍, 找到就直接定位到新位置 3. 真没了 → 打开最近还存在的上级目录, 并明确告诉你 三种情况都会给提示, 不会静默无反应。 --- ## 安全设计 清理工具删的是用户的数据, 所以默认必须是保守的: - **默认进回收站**(可还原), 而不是直接 `rm` - **dry-run 预演**先过一遍 - **三级分类**, `.git` 永远不可勾选 - **服务端硬性闸门**: 磁盘根目录、工具自身所在目录和程序文件, 一律拒绝 - **嵌套路径自动去重**(选了 `node_modules` 就不会再单独处理它内部的 `.vite`) - Electron 版把 Python 版那套「本地 HTTP + 随机 token」整个去掉了 —— 改成 IPC, **连端口都不占** ### 测试 118 项检查, 打包版也跑: ``` 规则校验 27 项 │ 后端 40 项 │ 界面 51 项 ``` 界面自检是**真的去点按钮**的 —— 包括点「停止」并验证扫描真的停下来了, 点路径并验证资源管理器真的打开了。 --- ## 获取 **绿色版**(免安装, 单文件): ``` dist\DiskScope-1.0.0-绿色版.exe ``` **从源码构建**: ```bash npm install npm run build # electron-builder + 写入图标与版本信息 npm run uitest # 界面自检, 会自动截图 ``` **Python 版**(只用标准库, 双击 `run.bat` 就能跑)也保留着, 两版共用同一套前端和同一份规则表。 源码: <https://github.com/star-sda/diskscope> --- ## 最后 工具的开发全程vibecoding,作为一名javaer真切感受到了AI的强大。

总在找刚才复制的东西,我给 Mac 做了个「剪巢」

我用 Mac 时,经常会被一件小事打断:**明明刚复制过的东西,转头又得找一遍。** 比如复制了一段代码,准备切回编辑器,中途又顺手复制了一个链接。等按下 `⌘V`,代码没了,链接来了。 只好回去翻网页、翻聊天记录。东西倒也没真丢,就是得再找一次。好不容易接上的思路,也跟着断了一下。 一次两次无所谓,碰多了,我就想:能不能把复制过的东西先留住,需要时叫出来,直接接着用? 于是,我做了 **剪巢**。 它是一个待在 Mac 菜单栏里的剪贴板历史工具:把复制过的文字、图片、文件引用存在本地,想用时按快捷键,搜一下,选中回车,就能贴回刚才的输入位置。 不用注册账号,也不用先整理分类。正常复制就行,需要它的时候再打开。 ![01-history-light.png](https://pic.code-nav.cn/post_picture/1723978257226919938/B9IUNtdGRnljV7TM.webp) *本文配图使用演示内容,图内保留了「演示」标识。* 源码和安装包已经放到 GitHub,**免费使用,MIT 协议开源**: **[项目主页](https://github.com/fenfeicn/jianchao) · [下载安装包](https://github.com/fenfeicn/jianchao/releases/latest)** 下面就聊聊,它具体替我省了哪些麻烦。 ## 找到那段代码以后,回车就能用 我最在意的,其实不是历史里能存多少条,而是找到以后,能不能顺手用上。 正在编辑器里写东西,想找回刚才那段代码,我希望整个过程是这样的: **点好输入位置 → 按 `⌥⌘V` → 搜关键词 → 选中 → 回车。** 剪巢会收起面板,回到刚才的应用,尝试直接粘贴。首次使用需要按引导开启系统权限,之后就不用每次再点复制、切窗口、按粘贴了。 如果要用的就在眼前,面板打开后还可以按 `⌘1` 到 `⌘9`,使用当前列表的前九条。先搜过关键词的话,数字对应的就是搜索结果。 这个小地方对我挺有用。有时只是在两三段内容之间来回搬,手不用离开键盘,事情就能接着做。 当然,也有只想把内容先拿出来的时候。比如还没决定发到哪个窗口,或者某个软件不接受直接粘贴。这时按 `⌘Return`,只把内容放回剪贴板,之后自己按 `⌘V` 就行。 默认呼出快捷键是 `⌥⌘V`,跟已有习惯冲突的话,可以在设置里改。 ## 找图片,还是让眼睛来比较快 文字还能靠关键词找,图片就不一样了。 有时候想找一张聊天里复制过的图,只记得大概长什么样。让我从一串文件名里认出来,确实有点为难。 所以剪巢的图片历史会显示缩略图,选中后在右边直接预览。扫一眼,就知道是不是那张。 ![03-image-preview.png](https://pic.code-nav.cn/post_picture/1723978257226919938/lRhfiNdBj9NvVU3Q.webp) 想看得更清楚,可以点击图片,或者按 `⌘Y` 放大查看。找参考图、截图,或者把一张图换个地方再发一次,都不用先另存到桌面。 微信、QQ 这类应用里的单张图片,只要复制时提供了可读取的图片数据或文件,剪巢就会尽量保存图片本身,减少临时文件失效带来的问题。具体还要看来源应用交给剪贴板的内容。 不过,**从 Finder 复制的文件和多文件记录,保存的是原文件引用**。原文件移动或删除后,记录可能失效;它不能代替文件备份。 ## 长内容不用从头读,先找要看的那一处 还有一种复制,内容虽然找回来了,人却不想看了。 一大段代码、一长串接口返回,铺满整个预览。我只是想确认某个字段,结果又得从头往下翻。 现在选中一条文字后,按 `⌘F`,就能在当前内容里查找。关键词会高亮,`⌘G` 找下一个,`⇧⌘G` 回到上一个。长文本也会自动显示行号,方便定位;复制内容时,行号不会一起带走。 ![04-preview-find-lines.png](https://pic.code-nav.cn/post_picture/1723978257226919938/3ELiqk3FLmVawNlu.webp) 这两个搜索,我一般分开用:上面的搜索框找“哪一条”,预览里的查找找“这一条的哪里”。 比如先搜「订单」,找到那段接口内容,再在里面找具体字段,比重新打开原来的页面省心。 预览里目前只做查找,没有加编辑和替换。真要改代码或文案,我还是更习惯把它贴回编辑器。 ## 常用的留一份,记不住的按日期找 有些内容,今天复制完就不会再用了。有些却隔三差五都要找一次,比如一条命令、一句常用回复,或者一段固定格式的文字。 后一种,我会直接收藏。 选中后按 `⌘D`,以后去收藏里找就好,不用等它被新记录一点点挤下去。 ![05-favorites.png](https://pic.code-nav.cn/post_picture/1723978257226919938/SrnezmmvsiHdvFcK.webp) 另一种情况也很常见:我记不住具体内容,但记得“应该是昨天复制的”。 那就先看昨天。 剪巢支持今天、昨天、近七天、近三十天和自定义日期范围,也能和关键词、内容类型一起筛选。比如只看昨天复制的图片,范围一下就小了很多。 ![06-date-filter.png](https://pic.code-nav.cn/post_picture/1723978257226919938/BPvBvzuL4FJWCocR.webp) 不是每次都得想起一个准确的关键词。有时候,“昨天”“是张图”,已经够用了。 ## 我希望它每天都能用,也每天都看得顺眼 剪贴板工具有点特殊:它平时没什么存在感,但一天可能会打开很多次。 所以我挺在意这个小窗口的样子。文字能不能一眼看清,列表和预览会不会挤在一起,切换时顺不顺手,都会影响我下次愿不愿意再叫出它。 现在的布局是左边找内容,右边看预览。背景用了 macOS 原生材质,橙色用来提示选中和重点操作,正文尽量保持清楚。支持跟随系统,也可以固定浅色或深色。 ![02-history-dark.png](https://pic.code-nav.cn/post_picture/1723978257226919938/0id46ca4G5sjBS4T.webp) 我想要的感觉是:打开时舒服,拿完东西就走。动画轻一点,别让粘贴一句话也要等半天。 它只待在菜单栏,不占 Dock 位置。应用图标是几张叠起来的纸和一枚橙色小夹子,菜单栏里则用单色图标。名字叫「剪巢」,也就是给这些零零散散的内容找个落脚的地方。 ## 留多久、占多少地方,自己决定 历史能找回东西,也会越积越多。尤其是图片,我不希望用着用着,才发现一个小工具占了一大块磁盘。 新安装默认保留 **30 天、最多 5,000 条,空间预算 500 MB**。想留得久一些,可以延长到半年、一年,也可以选不按时间清理;条数最高两万,空间最高 5 GB。 ![07-storage-settings.png](https://pic.code-nav.cn/post_picture/1723978257226919938/Ji8YSSJKwFvHDFJE.webp) “不按时间清理”仍然受条数和空间上限约束。收藏受到保护,不会按普通历史的规则自动清掉;如果收藏占满预算,应用会停止新增记录。 清理时,可以只清空普通历史,保留收藏。要连收藏一起删,再用需要确认的「删除全部」。真正重要的资料,还是建议存到文档或文件里。 另外,复制过的东西不一定都想留下。 剪巢没有账号、没有云同步,不会自动上传剪贴内容。**当前数据以明文保存在本地,没有加密,也不会自动识别所有敏感内容。** 所以我也留了暂停记录和「跳过下一次」:临时不想记录,就让它歇一会儿。可以暂停十五分钟,也可以等自己手动恢复;锁屏时同样会暂停,暂停期间的内容不会补录。 ![08-shortcuts.png](https://pic.code-nav.cn/post_picture/1723978257226919938/7u6DTP5YPZMV4F4R.webp) ## 装好后,花一分钟试一下 如果你也常常被“刚才复制的那个呢”打断,可以下载试试: **[前往 GitHub 下载剪巢](https://github.com/fenfeicn/jianchao/releases/latest)** 安装很简单: 1. 下载并打开 DMG,把剪巢拖进「应用程序」。 2. 推出安装镜像,从「应用程序」里打开剪巢。 3. 看完首次说明,点击「开始记录」。它会从这时开始保存,无法找回安装前已经被覆盖的内容。 4. 想用回车直接粘贴,按「设置 → 通用」里的引导开启系统权限,再按提示重新启动。 当前安装包尚未完成 Apple 公证,首次打开可能被系统拦截。确认下载来源后,可按提示到「系统设置 → 隐私与安全」中允许打开,无需关闭系统安全检查。粘贴权限入口在不同系统中可能叫「辅助功能」或「设备控制和数据访问」,跟随应用内引导即可。 然后,打开一个普通文本编辑器,依次复制三句话: > 第一杯咖啡 > > 第二杯咖啡 > > 第三杯咖啡 点好输入位置,按 `⌥⌘V`,搜「第一杯」,选中回车。 第一杯回来了,就会用了。 日常先记住这几个按键就够: | 想做什么 | 快捷键 | | --- | --- | | 呼出剪巢 | `⌥⌘V`,可修改 | | 选择并直接粘贴 | 上下方向键选择,`Return` | | 只复制,稍后自己粘贴 | `⌘Return` | | 使用当前列表前九条 | `⌘1`—`⌘9` | | 收藏或取消收藏 | `⌘D` | | 在当前预览中查找 | `⌘F` | | 退出查找/关闭面板 | `Esc`,逐层关闭 | 除呼出快捷键外,表里的操作都在剪巢面板获得焦点时使用。预览查找展开时,回车用于跳到下一个匹配。某个应用不接受直接粘贴的话,可以先用「只复制」,再自行粘贴。 当前版本为 **0.3.6**,安装包同时包含 Apple Silicon 和 Intel 架构,最低系统要求为 macOS 14。目前完整实机验证是在 macOS 27 的 Apple Silicon 设备上完成的,macOS 15、26 和 Intel 设备的体验,也欢迎大家反馈。 --- 我做剪巢,就是想少一点重复找东西,多一点顺着思路把事情做完的时间。 如果它也帮你找回了一段代码、一张图,或者一句刚被覆盖的话,那这个小工具就算帮上忙了。 我是 **hck**。用着哪里不顺手,欢迎到 [GitHub 提 Issue](https://github.com/fenfeicn/jianchao/issues),或者发邮件到 **87975531@qq.com**。告诉我系统版本、在哪个应用里、做了什么操作,比一句“不能用”更方便定位问题。截图或举例时,记得避开自己的隐私内容。 希望下次再问“刚才复制的那个呢”,我们都不用回去翻聊天记录了。

手把手教你设计一个成熟的 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**:服务端往浏览器单向推流,聊天界面那种边生成边显示的能力。 ## 目录 1. Agent 里哪部分是 Harness 2. 成熟度阶梯 3. 上下文的成本与上限 4. 架构上的两条界线 5. 核心部件 6. 状态与恢复 7. 失败路径的处理 8. 行为验收 9. 技术栈选型 10. 高频坑清单 11. 落地路线 --- ## 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. 上下文的成本与上限 这篇只留一句结论的话:**上下文有硬上限,越长越贵,而且每一轮都要重新付一次钱。** 四个具体后果: 1. **体积随轮数增长。** 每轮都要追加一条模型输出和一条工具结果,量随"轮数 × 每轮工具数 × 单项结果大小"滚动。读一篇长文或抓一整页网页,几万 token 就没了。 2. **注意力会衰减。** 技术上塞得下,模型对超长上下文中间部分的关注也会下降,关键信息容易被淹掉。 3. **越长越贵。** 每轮都要重发整个历史,长任务是成本的放大器。 4. **一次外置省的不是一轮。** 省下的是后面所有轮的重发量。 ### 大对象的处理顺序 面对文件、检索结果、网页正文、图片这类体积大的材料,成熟系统的处理顺序是固定的: ```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](https://github.com/dnwwdwd/Notus):本地优先的 AI Agent 笔记软件 [喵喵搜索](https://github.com/dnwwdwd/miaomiao-search):自托管多引擎网络搜索和远程MCP服务,覆盖必应、百度、微信公众号、DuckDuckGo、CSDN、GitHub 等多个搜索源,支持微信公众号、CSDN、掘金、GitHub README、Linux Do、小红书等文章内容抓取。 [Project Vibe Spec](https://github.com/dnwwdwd/project-vibe-spec):用于 Vibe Coding 项目的协作与交付治理:把用户确认、项目事实、设计决策、实现、验证和进度维持在同一条可追溯链路上,同时避免把所有上下文塞进一个巨大的 AGENTS.md。

Project Vibe Spec 大升级:我想解决 Vibe Coding 项目越写越乱的问题

> 开源地址:[github.com/dnwwdwd/project-vibe-spec](https://github.com/dnwwdwd/project-vibe-spec)如果对你有用,GitHub 上点个 Star 是最实在的支持 ⭐ 最近把自己开源的 `project-vibe-spec` 重构了一遍。 做这个 Skill 的原因一直没变。 现在用 Codex、Claude Code 做项目,写代码已经越来越省事。问题慢慢跑到了另外一边:项目做久以后,Agent 开始忘,文档开始乱,方案改过几轮没人记得,代码写完了也不知道到底验证到什么程度。 这种问题对 Vibe Coding 用户影响尤其大。 很多人不会逐行 Review 代码,我自己做一些项目时也不会每次把几百上千行 diff 从头看到尾。我更容易确认的是页面有没有做对、流程是不是我想要的、数据语义有没有问题、最后能不能跑。 一旦项目做几个月,光靠聊天记录就撑不住了。 这一轮 Agent 记得为什么这么改,换个 Session 可能就不知道了。PRD 还写着旧方案,代码已经跑到第三版。某个功能之前只做了 POC,过几轮以后 Agent 开始把它当成正式能力。数据库里多了一张表,几个月后没人知道当时为什么拆。 还有一种很常见。Agent 改完代码以后直接告诉你"已完成",结果测试没跑,UI 没打开,migration 没验证,Windows 也没测。 我之前写 `project-vibe-spec`,主要就是在补这些东西。 ## 旧版已经能管需求,但有点重 之前的 Project Vibe Spec 已经有一套比较完整的流程。 一个需求进来以后,会经过需求确认、REQ、方案决策、实现、测试、Progress 更新。数据库、migration、权限、安全、公开 API 这种不太好回退的改动,还要求先讨论方案,再让 Agent 动代码。 这套流程用了以后,项目确实没那么容易失控。 但它自己也慢慢长胖了。 `SKILL.md` 里面要规定需求怎么分类、什么算跨模块、什么时候建 REQ、什么时候写 DEC、数据库什么时候要确认、Progress 怎么更新、哪些文档一起改、最后怎么验收。项目自己的 `AGENTS.md` 也容易继续往里面塞规则。 时间一长,Agent 接一个很小的任务,也可能先读一堆跟当前工作没关系的内容。文档都在,Agent 的上下文反而越来越重。 ## 现在项目上下文拆成了四层 新版大概是这个结构: ```text AGENTS.md ↓ 判断当前任务应该读什么 DOCUMENT_MAP.md ↓ 找到项目现在认可的文档 PRD / PDD / Design / Flow / DEC ↓ 保存产品、技术、设计和决策 REQ / Bug / Progress ↓ 记录当前正在推进的工作 ``` `AGENTS.md` 现在会尽量保持轻。里面放仓库边界、安全规则、任务路由、高风险操作和通用验证要求。 比如: ```text 修改产品行为 → 读产品文档和相关 REQ 修改 UI → 读设计规范和对应产品规则 修改数据库 → 读技术设计和 DEC 修改 Agent / MCP / 检索 → 读技术设计和业务流程 修 Bug → 读 Bug 记录和受影响模块规则 ``` 表结构、接口字段、当前开发进度、某个功能的详细需求,不继续往根 `AGENTS.md` 里堆。Agent 改哪块,再加载哪块的上下文。 ## DOCUMENT_MAP.md 现在会告诉 Agent 哪份文档能信 这个改动看起来不大,我自己挺在意。 项目做久以后,仓库里经常会出现这种文件: ```text old-design.md architecture-v2.md architecture-final.md prd-new.md some-spike.md ``` 人还能根据名字和 Git 历史猜一下。Agent 可能全部读进去,然后旧方案、新方案、实验记录一起进上下文。 新版的 `DOCUMENT_MAP.md` 会给文档标状态: ```text 现行 参考 缺失 不适用 ``` 例如: ```text 产品事实 docs/product/spec.md 现行 旧版产品设计 docs/archive/product-v1.md 参考 UI Design 缺失 Desktop 架构 docs/desktop-architecture.md 现行 ``` 至少 Agent 进项目以后知道当前应该看哪一份。 ## init 也整个重写了 旧版 `init` 主要围绕 PRD/PDD 和项目总进度工作。 现在执行: ```text $project-vibe-spec init ``` Agent 会先把仓库过一遍。它会检查 `AGENTS.md`、`CLAUDE.md`、README、docs、需求、设计、DEC、Progress、测试、构建配置、schema、migration、部署脚本和一部分实际代码。先弄清楚项目已经有什么,再决定缺什么。 假设一个项目已经用了: ```text specs/product.md architecture/backend.md docs/design-system.md adr/ ``` 新版不会再硬生生补: ```text docs/PRD.md docs/PDD.md docs/UI_GUIDE.md Decisions/ ``` `DOCUMENT_MAP.md` 里直接记现有路径: ```text 产品事实 → specs/product.md 技术事实 → architecture/backend.md 设计规范 → docs/design-system.md 架构决策 → adr/ ``` 跑 `init`,更接近给现有仓库做一次整理和审计。 ## 没有 PRD,就先记没有 以前为了把结构补齐,很容易生成一堆空模板。`PRD.md` 有了,`PDD.md` 有了,`UI_GUIDE.md` 也有了,里面没有多少能指导 Agent 的内容。 这轮把这个行为改掉了。 项目没有 PRD,可以直接记: ```text 产品事实:缺失 ``` 没有设计规范,而且项目根本没有 UI,也可以记: ```text 设计规范:不适用 ``` 后面开发真的需要产品规则,再补 PRD。模板现在只在项目缺这份信息、同时后续工作又需要它的时候才创建。 ## 历史功能也不用补几十个 REQ 一个已经做了几个月的项目第一次跑 init,如果硬套需求台账,很容易一次性生成很多历史 REQ。项目里已经有十几个功能,就补十几个需求记录。这些文件看起来规范,之后基本没人维护。 现在已有功能直接进入"当前实现基线"。 例如: ```text Agent Loop 已验证 PDF 解析 已实现待验证 多模态 PDF 已通过(POC) ``` 从这次初始化往后,新需求和还在推进的大任务再进入 REQ。需求台账里留下来的,基本都是后面还会继续看的内容。 ## "代码已经写了"单独变成一个状态 这次加了一个状态: ```text 已实现待验证 ``` 现在 Progress 有这些状态: ```text 已验证 已实现待验证 已通过(POC) 进行中 待开发 待拆分需求 待澄清 不纳入 ``` 加这个状态就是因为 Coding Agent 太容易把代码完成和功能完成混在一起。 仓库里已经有实现,只能说明代码存在。测试没跑完,就写"已实现待验证"。只验证过一个 Demo,就写"已通过(POC)"。有对应测试、构建结果或者真实用户路径验证以后,再写"已验证"。 对于不太看代码的人,这个区分比多一份技术文档有用得多。至少你问 Agent"这个功能做完没有",它不能只因为搜到了代码就回答完成。 ## 数据库这种改动我还是卡得很死 这轮删了不少文档负担,但数据库规则没放松。 Agent 想加表、加字段、改索引、迁历史数据、删数据或者改 ORM schema,还是先调查。我要看到这个字段表示什么,谁写,谁读,旧数据怎么办,能不能为空,有没有唯一约束,需要什么索引,上线怎么迁,失败以后怎么处理。方案确认以后再改。 Vibe Coding 里面,数据库被连续改错几轮,比一个按钮颜色错了麻烦得多。这块宁愿多一次确认。 ## 这版对不 Review 代码的人有什么用 Project Vibe Spec 解决不了所有代码质量问题。竞态条件、慢 SQL、隐藏的安全问题、边界异常,该测还是得测,该 Review 还是得 Review。 我更关心的是另一个问题。 很多 Vibe Coding 项目做着做着,用户已经不知道项目现在是什么状态了。 需求当时怎么确认的?Agent 为什么用了这个方案?这个实现是临时的还是正式的?POC 后来有没有进正式版本?文档和代码现在该信哪个?哪些地方写完了还没测?下一次开新 Session 从哪里继续? 这些信息如果全在聊天记录里,换个 Agent、换个工具或者隔一个月回来,很容易断。 新版 Project Vibe Spec 会尽量把这些信息留在仓库里。以后无论用 Claude Code 还是 Codex,Agent 先从 `AGENTS.md` 进入,根据任务找到 `DOCUMENT_MAP.md` 里的现行文档,再读对应的需求、决策和进度。 聊天记录可以丢,项目自己的状态还能接着用。 这次重构完以后,Project Vibe Spec 少了一些"必须创建什么文件"的规定,多了一套让 Agent 找到当前可信信息的办法。对大量用 Agent 写代码、又不会每次认真 Review 全部 diff 的开发方式,这比继续加几十条规范有用。 如果这套思路对你有帮助,去 GitHub 点个 Star 吧:[github.com/dnwwdwd/project-vibe-spec](https://github.com/dnwwdwd/project-vibe-spec) ⭐

喵喵搜索开源了——一个可自托管的多引擎联网搜索与 MCP 服务

大家好,我是汉堡🍔。 今天把一个做了一段时间的项目开源了:**喵喵搜索**(miaomiao-search)。 GitHub 地址:https://github.com/dnwwdwd/miaomiao-search 如果觉得有用,欢迎点个 ⭐ Star,这是我持续更新最大的动力。 --- ## 它是什么 一句话:**可自托管的多引擎联网搜索 + Remote MCP 服务**。 你可以把它部署在自己的机器或懒猫微服上,然后让 AI 客户端(比如 Codex、小龙猫)通过 MCP 协议调用它来搜索互联网、读取网页正文——完全在你自己的控制下,不依赖第三方中转。 --- ## 核心功能 **聚合 11 个搜索源**,一次搜索覆盖: - Bing、Baidu、DuckDuckGo、Sogou - CSDN、掘金 - GitHub(支持可选 Token) - B站(公开接口,返回官方 CDN 封面) - Exa、Firecrawl、Tavily(需要用户自己的 API Key,按用户加密存储) **读取网页正文**,支持 JavaScript 渲染回退(生产镜像内置 Chromium)。 **Remote MCP 服务**,通过 Streamable HTTP 暴露 MCP Tools,外部客户端用 Access Token 接入,懒猫应用间可以委托鉴权,不需要额外 Token。 **管理门户**,可以管理搜索引擎开关、缓存、限流、历史记录和审计日志。 **账号体系**,支持懒猫 OIDC 和本地账号密码两种登录方式。 --- ## 技术栈 - **前端**:Next.js 16、React 19、TypeScript、Tailwind CSS v4 - **后端**:Fastify 5、SQLite、Drizzle ORM - **MCP**:MCP TypeScript SDK v2 - **搜索底层**:`open-websearch@2.1.11` daemon 适配器 单实例运行,数据用 SQLite 持久化,按懒猫 UID 隔离用户数据,包内不含数据库或密钥。 --- ## 本地跑起来 要求:Node.js 20+、pnpm,以及一个可通过 HTTP(S) 访问的 Open-WebSearch daemon。 ```bash pnpm install ``` 启动 Open-WebSearch daemon(Windows PowerShell 示例): ```powershell $env:SEARCH_MODE="request" $env:USE_PROXY="true" $env:PROXY_URL="http://127.0.0.1:7890/" pnpm --filter @miaomiao-search/server exec open-websearch serve --port 3210 ``` 再分别启动服务端和门户: ```bash pnpm server:dev pnpm dev ``` 默认地址: - 门户:`http://127.0.0.1:3000/` - 服务端:`http://127.0.0.1:3001/` - MCP Endpoint:`http://127.0.0.1:3000/mcp` --- ## 安全设计 几个关键点说一下: - **实例密钥**派生会话、MCP Token Hash 和设置加密密钥,生产环境由懒猫清单注入,不能写进仓库。 - Exa、Firecrawl、Tavily、GitHub 的凭据在门户"引擎管理"中按用户保存,服务端只在当前用户数据库中存加密值。 - 外部 MCP 客户端用 `Authorization: Bearer <Token>` 接入;懒猫应用间调用由 ingress 注入 `X-HC-SOURCE` 和 `X-HC-USER-ID`,委托鉴权,不需要额外 Token。 - 门户不把 `X-HC-*` 头当登录凭据,所有受保护 API 将当前 UID 与应用会话绑定校验。 --- ## 许可 Apache-2.0,详见仓库 [LICENSE](https://github.com/dnwwdwd/miaomiao-search/blob/main/LICENSE)。 --- 项目还在持续迭代中,欢迎提 Issue 和 PR。 如果觉得有用,**点个 Star** 是对我最大的支持 ⭐ 👉 https://github.com/dnwwdwd/miaomiao-search

我是如何学习大模型应用开发的

## 大模型应用开发的一些概念关系 ![image 1.png](https://pic.code-nav.cn/post_picture/1616049613767180290/Bn0snnG5XY7wSruV.webp) 上面整理出来的是我自己对于大模型应用开发的理解,也结合了一些技术和概念的变化时间线,仅供各位朋友参考,不一定准确,我觉得大模型应用开发最重要的是这三个东西:Agent Loop、Context Engineering、Harness Engineering > 最近好像又新出了很多层的概念,在harness engineering之上,但是我个人现在并不喜欢,所以没去做更深入的了解,我觉得新出的很多概念,没有出现新的基础元素,只是融合现有的,然后加深了一下理解,所以我个人的想法是:了解即可,知道有这么个东西就可以 > > - Agent Loop是运行的核心,是一切的基础,用户输入任务,LLM输出指令,工具执行并返回结果,不断的循环直到结束 - Context Engineering在静态系统提示词结构不断稳固之后,发现Agent Loop在运行中是需要大量的动态信息的,并且常见有效的动态信息是用户记忆、会话历史记录、用户输入、系统提示词、工具定义等,那么这个时候就有了Context Engineering这个概念,上下文是Agent Loop有效的关键,在Agent长程运行中,上下文不断积累,会出现各种各样的问题,漂移、污染、干扰、冲突等,所以我们需要上下文管理的方法,需要渐进式加载的概念 - Harness Engineering是Agent稳定运行的关键,它为Agent搭建运行空间,设计Agent的能力结构、协作机制和反馈闭环,其不能仅仅从约束的角度去理解,更应该是在创造Agent的运行环境,让LLM可以做到原本无法做到的事情 ## 推荐的学习顺序 然后我按照自己的理解,大概梳理一下学习的顺序和方向,给学习的朋友一些“经验之谈”,当然下面的这些概念我都单独梳理过一些文章和实践经验,会搭配一些代码讲解,感兴趣的朋友可以借助这个学习手册来更好的了解 > 《大模型应用开发 -上下文工程与运行空间实践指南》:https://github.com/WakeUp-Jin/Practical-Guide-to-Context-Engineering > > ![image.png](https://pic.code-nav.cn/post_picture/1616049613767180290/jOWqhqWZHj9PPZCB.webp) 还有一些其他值得多聊的事情,我这边继续补充一些我自己的理解: 对于RAG和知识图谱的理解,我认为这个来源于“背景信息”这一种类型的上下文,我没办法准确的去下定义,我只是有一种淡淡的理解。 用户输入的问题,LLM需要解决,是需要一些背景信息的,有时候用户可以手动补充,模型也可以依靠推理能力去补充澄清,但更多的情况下,LLM需要依靠工具系统去动态的检索到这些“背景信息”,那么检索流程中最关键的手段就是:RAG和知识图谱,我甚至觉得Harness Engineering诞生在Agent领域中,也是因为“背景信息”作为一个突破口出现的。 上下文管理是一个值得深入探索的方向,Agent借助Skill来达到上下文层面的自进化,是一个上下文管理的具体应用,我相信这个里面还有很多有趣的东西,值得去深挖,目前来看比较常见的是:上下文压缩,文件存储检索,上下文编排,渐进式披露等。 在工具设计中,如果要有一层“宗门长老“的概念的话,我觉得是Edit、Bash、Read、Glob、Grep、Wirte这“六位宗门长老”,Bash工具甚至可以是“大长老”级别的。而目前最有难度的是Bash工具运行时的安全和输出问题,而Browser Use和Computer Use是为数不多的有趣的方向了 Agent形态方面,现在还在不断的变化吧,之前大家还会讨论Agent是自主运行好,还是协同驾驶好,现在看来各有各的的领域优势了,在任务确定的领域中,自主运行借助如今的模型推理能力完全可以覆盖住,在创作领域中,协作驾驶还是有一席之地的,用户在Agent输出的产物中,手动的进行各种小微调,同时也可以直观的看到Agent的中间执行链路。 单智能体和多智能体的方式,我觉得单智能体\+一个SubAgent的能力,其实就非常好用啦,如果设计成比较纯净的多智能体,成本是可以下降的,但混乱系数会大大增加,不仅对于某些模型的能力要求高,同时上下文管理的难度是指数级上升的,还会添加多智能体协作的概念,这是一个很有魅力的领域,很有挑战的领域。 接下来是我近期最感兴趣的方向了,也是大模型应用开发工程师们最容易忽略的,但在Agent开发中是最重要的,甚至可以说,没有它存在,你的Agent只是一个没有方向的经验产物。 没错,这个东西就是Agent评估,在构建一个Agent之前,或者说在设计一个Harness之前,你需要先存在一批数据集,以此来确定Agent的能力边界和迭代方向,Agent评估在开发中可以帮助你不断打磨提示词、上下文、工具等。 同时在数据集评估的过程中,通过对于执行链路和结果的观察,你会产生一些Agent垂直化的方向,最后你可以通过评估结果来确定它在混乱环境中的运行情况。Agent评估非常值得成为构建的第一步 ## 我的学习经历 我是从24年大学毕业的时候,大概6月份开始学习的,当时第一个需求是批量根据一个固定的提示词生成文章,我当时调用的是百川大模型,当时要毕业答辩啦,所以回了一趟学校,我调试了很久,我记得是在宿舍晚上10点左右调通的,就是一个简单的for循环的API调用,那是第一次看到LLM API的请求格式。 之后在接触到LangChain,里面有一个概念叫做检索器,里面涉及到向量知识库\+嵌入模型,当时我们团队有一些语义上的需求检索,所以借助这个机会,我去了解LangChain和向量数据库,在学LangChain的时候,很多概念都非常懵逼,不知道什么意思,封装的在我看来很复杂,当时硬啃了好几天,慢慢的有了一些感觉,LLM供应商的封装,tools方法的定义和使用,链式调用等概念 期间不断了解到各种框架,也在系统性的学习提示词工程,我对于LangChain和LangGraph学的比较多,算是我的入门框架。再后来 Cursor 等 Coding Agent 开始火热,我被 Agent 协同的模式吸引,开始关注工作流编排、工具定义、知识图谱等方向,也用飞书多维表格 \+ RAG \+ LangGraph 搭了团队第一个 Agent 项目 在做一个Agent项目的过程中,我形成了自己对于上下文工程的理解,一个Agent是否有效,取决它注入那一刻的上下文是否正确,《大模型应用开发 \-上下文工程与运行空间实践指南》这个开源项目也是这个时候开始创建的,我开始深入了解一些技术细节,RAG的检索策略,工具的定义,LLM模块的设计,上下文编排。 同期ClaudeCode、gemini\-cli、opencode开始出现,我就开始研究这些开源框架,研究cli 的方式,发现不需要RAG,通过工具形成的本地检索,也就是搜索Agent,随着模型能力的提升,这种方式变得极其有效 这个时候我们团队的第一个Agent项目的V2版本开始迭代了,工作流的方式只能让效果还行,在模型不断提升的能力面前,工作流不仅得不到提升,反而成为了好结果的束缚,开始显露出鸡肋的感觉了,我当时在一篇文章看到“模型应用的开发,要站在模型的同一侧,不要站在模型的对立面”。所以V2版本我们决定采用多智能体\+自定义工具设计的方式。 在之后关于如何让一个Agent可以长时间稳定运行的问题开始出现了,一个Agent可以不断的运行多久才会崩溃,上下文会变得极其混乱,这个时候的上下文管理开始慢慢出现了,以此同时Harness Engineering也开始诞生,OpenAI和Anthropic都发布过工程博客的文章,一个是专注于上下文工程和反馈闭环,一个专注于编排,所以我结合这两篇文章,开始构建自己对于Harness Engineering的初步理解,并且后续也在不断的调整 这个时候我也在更深入具体的了解Coding Agent都有哪些工具,这些工具是如何定义的,上下文的压缩策略是什么,指令又有哪些,Skill对于上下文工程意味着什么等问题 现在我的方向是多智能体协作,上下文管理,Agent评估,Harness的自进化,我觉得这几个方向很有意思,值得做更多的探索和学习 还有一个比较有意思的是DeepSeek Harness的一切皆插件的理念,存在着一种可能:Agent Loop可以更换,我很喜欢这一点,它让沉默的Harness仿佛再次焕发生命力,或许可以这么理解,大模型应用开发,一切皆可能,当Agent要面对复杂环境时,静态固定的方式很大程度会失效,开发者不可能在一开始就穷举复杂环境的问题,所以动态自进化的方式是值得尝试的,甚至很可能是一种很好的解法 ## 我的学习资源的推荐 - 飞书里面的通往AGI之路知识库:[通往AGI之路](https://waytoagi.feishu.cn/wiki/QPe5w5g7UisbEkkow8XcDmOpn8e) - Anthropic工程实践文章:https://www.anthropic.com/engineering - Claude团队博客:https://claude.com/blog - Anthropic的研究文章:https://www.anthropic.com/research - Cursor团队博客:https://cursor.com/cn/blog - Lilian个人博客:https://lilianweng.github.io/ - ClaudeCode的文档:https://code.claude.com/docs/en/overview - LennysPodcast的视频:https://www.youtube.com/@LennysPodcast - Pi的文档和源码:https://github.com/earendil-works/pi、https://pi.dev/docs/latest - 我也在做一个相关的开源项目,感兴趣可以看看《大模型应用开发 -上下文工程与运行空间实践指南》:https://github.com/WakeUp-Jin/Practical-Guide-to-Context-Engineering ## 我对于大模型应用开发的理解 在我自己开源项目中,有2篇我很喜欢的博客,一篇是《两种世界的交互形态:协同Agent与自主Agent》,另外一篇是:《编程 Agent 的工程实践:来自 OpenAI 与 Anthropic 的实战经验》,这个开源项目的理念我觉得也不错:上下文工程是设计原则,Harness是建造目标 在这一大段时间的学习和实践中,我也在不断的想一个问题,对于大模型应用开发来说,什么样的思维和理解是有帮助的呢? 我稍微整理了一些想法(**谨小认知,仅供参考)** 1. 务实的品质可以让大模型应用开发工程师走的更远,做的更多,务实让开发者不困于“过度冗长且孤立”的思考中,可以寻找到简单有效的解决方案 2. 像你的Agent一样思考,借助执行日志,观察上下文工程的设计缺陷,观察Harness的运行漏洞 3. **人类观察Agent的行为很重要,Agent观察人类的行为也很重要** 4. 「Lead, don't follow」去从第一性原理思考事情的本质,去发现和构建新的东西 5. 不要轻易放弃底层技术的学习,它们会极大的给你提供新的视角,要学会站在巨人的肩膀上 我是 WakeUp-Jin,目前在做两件事: 1. 整理一套上下文工程与Agent Harness实践指南 —— 上下文工程是设计原则,Agent Harness 是构建目标 2. 开发 Actspace Agent—— 一个本地 Agent 桌面应用,从 Coding Agent 开始,最终愿景是彻底释放模型能力 GitHub: https://github.com/WakeUp-Jin

拆解 GitHub 趋势榜 TradingAgents:多 Agent 辩论架构与本地运行实测

大家好,我是不会喷火的小火龙。今天我们来拆解一个最近频繁登上GitHub趋势榜开源的多智能体金融量化交易全流程框架。 在平常让大模型直接分析股票,往往会得到一份四平八稳的研报。但在真实交易里,单向叙事暗藏风险:只要挑选不同的技术指标或新闻切片,无论看多还是看空都能写出逻辑自洽的分析。金融决策要在信息不对称下权衡概率与风险,并不存在现成的标准答案。 ## 一、为什么单一 Prompt 难以胜任金融决策 用大模型做交易策略和财报分析的尝试很多,但单一 Prompt 很容易陷入确认偏误(Confirmation Bias)。当系统接收到偏多的技术指标时,倾向于给出顺势做多的结论;当面对负面宏观数据时,又很容易偏向防守。提示词再长,也很难在同一个上下文内保证对抗立场的客观性。 机构投研流程的设立,初衷就是利用角色分权来暴露漏洞。技术面与基本面关注的周期不同,情绪面与宏观面各有噪音。多空研究员各执一词,风控团队负责收紧止损,最终由投资经理做出权衡。TradingAgents 的核心思路就是将这套博弈逻辑转化为状态图上的确定性流程。 传统量化或对冲基金在做投资决策时,通常会有分工明确的角色:收集数据的分析师、互相挑刺的多空研究员、评估尾部风险的风控部门,以及拍板的投资经理。TradingAgents(论文 arXiv:2412.20138)把这套组织结构搬进了多智能体系统。项目开源后一度登上 GitHub 趋势榜,收获了 7000 多 Star。它的价值不仅在于生成金融研报,也在于为复杂决策型 Agent 系统的分工与对抗机制提供了一个参考实现。 下图是 TradingAgents 的完整协作流水线,从多源数据提取、观点辩论到风控评估,均由专门的 Agent 节点承接: ![image.png](https://pic.code-nav.cn/post_picture/1612112775822180354/8dPCJJMsfOyqJP3E.webp) ## 二、核心架构:模拟基金投研的 5 阶段流水线 TradingAgents 的设计重点在于各个 Agent 之间的信息流向和制约关系。系统将决策链路划分为五个阶段: ### 1. 分析团队:四维度并行采集数据 系统启动后,四位分析师 Agent 并行运行,分别处理各自的数据源: - Market Analyst(市场分析师):通过 Yahoo Finance 获取价格与常用技术指标(如 OHLC、移动平均线、MACD、RSI、ATR、布林带等),整理技术面研报。 - Fundamentals Analyst(基本面分析师):梳理财务指标、估值水平与同业对比。 - News Analyst(新闻分析师):汇总宏观新闻,评估地缘政策、利率环境等宏观因子对标的的影响。 - Sentiment Analyst(情绪分析师):抓取 StockTwits、Reddit 等平台上的散户讨论,统计情绪比例。 四个分析师彼此独立,各自生成文件。这种隔离避免了单一维度的强结论过早干扰其他维度的信息提取。 ![image.png](https://pic.code-nav.cn/post_picture/1612112775822180354/PTDRNbJJz0MLZhqV.webp) ### 2. 多空辩论:结构化对抗 汇总四份分析报告后,流程进入多空对抗阶段。 系统没有让一个 Agent 同时总结利弊,而是分别设立了 Bull(多头研究员)和 Bear(空头研究员)。多头先列出自己的支撑论据,空头针对这些论点逐一反驳并补充利空因素,多头再就反驳进行二次防守。辩论轮数可以在配置中调整(默认为 1 轮,可设为 2 轮)。 辩论结束后,由 Research Manager(研究主管)进行裁判。研究主管负责梳理双方在关键价位和宏观逻辑上的分歧点,评估论据支撑强度,最终形成一份综合研究总结。 拆分对抗角色能够有效避免大模型常见的调和倾向,迫使模型把两端的论据和风险都完整暴露出来。 ### 3. 交易员:制定具体参数 研究主管的总结交由 Trader Agent。交易员不重复分析基本面或技术面,而是将结论翻译为具体的交易计划:方向(买入 / 卖出 / 持有)、进场区间、止损价位、目标点位和建议仓位。 ### 4. 风控委员会:三方评估 交易计划提交后,由三位风控分析师组成委员会进行审核: - Aggressive Analyst(激进派):关注盈亏比与潜在机会,侧重执行可行性。 - Conservative Analyst(保守派):聚焦极端回撤与黑天鹅事件,倾向于压缩风险敞口。 - Neutral Analyst(中立派):平衡机会与潜在风险,评估赔率。 三位风控人员同样会针对仓位大小、止损距离是否合理等细节展开讨论。 ![image.png](https://pic.code-nav.cn/post_picture/1612112775822180354/mEQb2eMXtgK7RnVA.webp) ### 5. 投资经理:最终裁决 最后,Portfolio Manager(投资组合经理)统揽所有上游报告,包括分析师数据、多空辩论过程、交易员方案以及风控委员会的审议意见,给出最终结论:评级(Buy / Hold / Sell)、执行摘要、投资逻辑以及观察周期。 五个阶段构成完整的流水线,使每个环节的观点都会在下一环节接受检验。 ## 三、工程机制:反思记忆与断点恢复 TradingAgents 在工程实现上有两点值得注意:长流程容错与跨周期复盘。 ### 断点恢复(Checkpoint Resume) 一次完整的研报生成包含十余次模型调用,耗时通常需要几分钟。如果中间因为网络超时或 API 限流中断,从头重跑会浪费调用成本与时间。 TradingAgents 基于 LangGraph 的 SQLite 检查点功能实现了节点级状态保存。运行时加上 `--checkpoint` 参数后,每个 Agent 节点执行完毕都会把状态写入数据库。任务中断后重新启动,会自动从断点处继续执行。 ### 决策日志与反思机制(Reflection) 每次做出评级后,系统会把决策摘要记入本地的 `trading_memory.md`。当后续再次分析同一标的时,系统会自动执行几步复盘: 1. 调取上一次的决策(包括当时的评级、入场价与设定周期)。 2. 获取该标的在决策之后的实际走势。 3. 计算实际收益率以及相对基准指数(如 SPY)的超额表现。 4. 将偏差反思整理为文本,作为上下文注入到本次投资经理的 Prompt 中。 通过这套闭环,历史判断的偏差能够作为经验输入给下一次决策。 ## 四、本地部署:Docker 环境与模型配置 ### 1. 获取代码与构建镜像 ```bash git clone https://github.com/TauricResearch/TradingAgents.git cd TradingAgents cp .env.example .env docker compose build ``` ### 2. 模型接入配置 TradingAgents 原生集成了多家主流 LLM 接口,同时也支持任何遵循 OpenAI 协议的自定义端点。 以火山方舟 Agent Plan 为例,在 `.env` 中添加对应配置: ```bash # 采用兼容协议 TRADINGAGENTS_LLM_PROVIDER=openai_compatible # 火山方舟 Agent Plan 端点 TRADINGAGENTS_LLM_BACKEND_URL=https://ark.cn-beijing.volces.com/api/plan/v3 # 填入申请的 API Key OPENAI_COMPATIBLE_API_KEY=your_api_key_here # 指定模型名称(支持使用 deepseek-v4-flash、kimi-k3 等平台原生名称) TRADINGAGENTS_DEEP_THINK_LLM=deepseek-v4-pro TRADINGAGENTS_QUICK_THINK_LLM=deepseek-v4-flash ``` 使用火山方舟 Agent Plan 的便利之处在于可以直接配置模型名,无需单独创建 Endpoint 接入点。 ### 3. 网络与环境问题处理 TradingAgents 默认通过 Yahoo Finance 获取行情。在部分网络环境下,容器内请求 `query2.finance.yahoo.com` 可能遇到 SSL 握手断开的报错。 可通过将宿主机代理映射进容器来解决,同时将国内的模型服务地址排除在外: ```bash # 容器使用宿主机本地代理访问外部行情 HTTP_PROXY=http://host.docker.internal:7890 HTTPS_PROXY=http://host.docker.internal:7890 # 国内模型端点走直连 NO_PROXY=localhost,127.0.0.1,ark.cn-beijing.volces.com ``` 另外需要注意:在 Mac 上使用 Docker Desktop 时,如果默认的 `desktop-linux` context 受到网络代理影响导致 `docker compose run` 无响应,可以切换到原生 socket 连接: ```bash docker context create direct \ --docker "host=unix://$HOME/Library/Containers/com.docker.docker/Data/docker.raw.sock" docker context use direct ``` ### 4. 报告文件持久化挂载 容器默认将研报保存在内部路径。可以在 `docker-compose.yml` 中挂载目录,方便直接在宿主机查看: ```yaml services: tradingagents: volumes: - ./reports:/home/appuser/app/reports ``` ### 5. 启动交互式分析 ```bash docker compose run --rm tradingagents ``` 启动后会弹出交互提示,输入标的代码(如 `NVDA`、`BTC-USD`)、基准日期、参与的分析师角色、辩论轮次和输出语言即可启动流程。 ![image.png](https://pic.code-nav.cn/post_picture/1612112775822180354/8QCvZ5gukfsXPMFq.webp) ## 五、实测复盘:BTC-USD 完整研报 这里使用火山方舟的 `deepseek-v4-flash` 模型,以 BTC-USD 为标的做了一次完整运行。 终端输出展示了各模块的执行耗时与归档路径: ![image.png](https://pic.code-nav.cn/post_picture/1612112775822180354/z1SXymdftGul2HCx.webp) 分析全程耗时约 6 分钟,生成了一份 186KB、1244 行的 Markdown 研报。输出文件按执行阶段分层归档: ``` reports/BTC-USD_20260910_130329/ ├── 1_analysts/ # 各分析师独立数据报告 │ ├── market.md # 8 项技术指标核算 │ ├── sentiment.md # 社区情绪量化 │ └── news.md # 宏观新闻梳理 ├── 2_research/ # 多空对抗记录 │ ├── bull.md # 看多观点 │ ├── bear.md # 看空反驳 │ └── manager.md # 研究主管综合 ├── 3_trading/ │ └── trader.md # 交易执行计划 ├── 4_risk/ # 风控三方讨论 │ ├── aggressive.md # 激进派 │ ├── conservative.md # 保守派 │ └── neutral.md # 中立派 ├── 5_portfolio/ │ └── decision.md # 投资经理裁定 └── complete_report.md # 完整报告合集 ``` ### 技术指标校验 Market Analyst 抓取并计算了当时 BTC-USD 的多项指标: | 指标 | 数值 | 研报解读 | |:---|:---|:---| | 收盘价 | 77,133.63 | 基准价格 | | 10 EMA | 78,428.41 | 现价低于该线,短期动能衰退 | | 50 SMA | 70,414.24 | 现价高于该线约 9.5%,中期趋势尚存 | | 200 SMA | 69,965.22 | 50/200 均线金叉,长期均线呈修复状态 | | RSI | 54.89 | 从高位超买(>80)回落至中性区 | | MACD 柱 | -641.58 | 死叉已现,动能转弱 | | ATR | 2,142.89 | 日均波动约 2.8%,止损需预留安全空间 | 在报告细节中,分析师还比对了 `get_stock_data` 与 `get_verified_market_snapshot` 接口返回的收盘价(差异约 54 美元,占比 0.07%),并在报告中明确说明统一以验证快照为准。 ### 多空辩论要点 多空双方在核心逻辑上的交锋较为具体: 多头论点: > 价格保持在 50 日均线上方 9.5%、200 日均线上方 10.2%,且 200 日线刚掉头向上,整体属于均线多头修复。且回调阶段成交量较 8 月突破时期缩减超过一半,缺乏主力集中派发的特征。 空头反驳: > 均线系统具有滞后性,难以直接预测短期二元风险。当前面临美国关键宏观数据公布、欧洲央行加息以及美债收益率走高的多重宏观压力,短期内指标均线无法对冲宏观利空冲击。 风控人员的讨论则聚焦在操作节奏上: > 激进派的短线试多方案将止损设在 75,400,距离现价仅 2.2%,但当前日均 ATR 达到 2,143 美元(波动率 2.8%)。这意味着在数据发布前夕,普通盘中插针就会轻易扫掉止损。 ### 终审结论 投资经理最终给出的评级为 Hold(观望): > 中期结构偏多,短线动能偏空,且宏观上存在待公布的重大经济数据。在这种信号冲突的节点,维持现有持仓、不盲目开新仓是较优选择。等待宏观事件落地后,若价格在 76,000–77,100 区间企稳,或放量收复 10 EMA(78,428),再右侧轻仓介入。 系统没有机械地在多空结论中二选一,而是在多项指标冲突时选择等待右侧确认条件,并给出了明确的观察窗口。 ## 六、现状与实盘距离 TradingAgents 验证了多角色对抗在提升复杂分析质量上的有效性,但在走向实际自动化交易前,依然存在几处现实门槛: 1. 时效与延迟:端到端完整分析涉及十余次推理,单次运行约 3 到 6 分钟。这种耗时决定了它更适合日线级别的波段分析与复盘,无法应对秒级的日内撮合与高频风控。 2. 数据生态接入:原版基于国外数据源构建。若用于 A 股或其他本土市场,需要适配本土行情接口与研报来源。 3. 执行层的衔接:系统当前输出的是逻辑分析和参数建议,并不包含与券商交易接口的直接连通,订单管理、滑点控制和仓位风控等工程模块需要单独开发。 4. 策略回测工具链:框架主要侧重于单次决策的推理过程,目前缺少开箱即用的批量历史回测流水线,较难大规模统计在不同市场周期下的胜率与收益分布。 作为辅助工具,TradingAgents 能够帮助投资者在做决策前快速梳理多空逻辑、罗列潜在风险并量化波动指标。作为多 Agent 系统项目,它在角色拆分、辩论裁决和状态机容错上的处理方式,为探索复杂任务下的 Agent 协作提供了一个清晰的工程参考。 --- 项目地址:https://github.com/TauricResearch/TradingAgents 论文引用:arXiv:2412.20138 免责声明:TradingAgents 属于研究类开源项目,本文仅作技术与架构拆解,文中所涉及标的与实测数据均不构成任何投资建议。

下载 APP