☰
Harness架构Agent实战:20万行代码与40亿token的工程优化
2026/9/29 18:04:28 网站建设 项目流程

1. 先搞清楚这个项目到底在做什么

一个人,九个月,20万行代码,每个月消耗40亿以上的token——这几个数字放在一起,第一反应大概率是"这不可能"。但如果你真的动手做过一个基于Harness架构的Agent应用,就会明白这些数字背后其实是一套非常具体的工程选择,而不是什么玄学。

先把概念对齐。这里说的Harness,不是指某个具体的产品,而是指一类Agent执行框架的设计范式:把大模型的推理能力当作一个可调度的内核,外面套一层"挽具"(Harness),负责工具调用、上下文管理、状态持久化、错误恢复、任务编排。Claude Code、DeepSeek Harness、Pi Agent这些,本质上都是Harness思路下的不同实现。你要做的应用,就是在这个范式上搭出自己的业务层。

那20万行代码是怎么来的?不是手写20万行,而是框架代码+工具定义+提示词模板+状态机+测试用例+配置的总和。一个成熟的Harness应用,工具定义(Tool Schema)往往就占了几万行,因为每个工具都要写清楚参数、返回值、错误分支、权限边界。再加上Markdown解析、Obsidian知识库对接、多轮对话状态管理这些模块,20万行并不夸张。

每个月40亿token又是什么概念?按一个月30天算,平均每天1.3亿token。如果单次Agent任务平均消耗5万token(包含系统提示、工具定义、历史上下文、工具返回结果),那一天就是2600次任务调用。对于一个需要持续跑知识库索引、文档转换、多轮推理的应用来说,这个量级是合理的。关键在于——token不是烧掉的,是设计出来的。你怎么切分上下文、怎么复用缓存、怎么压缩历史,直接决定了成本是40亿还是4亿。

这篇文章适合谁看?如果你正在用Claude Code、DeepSeek Harness或者自己搭Agent框架,想搞清楚一个长期运行的Harness应用该怎么设计架构、怎么控制成本、怎么和Obsidian这类知识库打通、怎么处理Markdown的各种坑,那接下来的内容应该对你有用。如果你只是好奇"一个人怎么能写20万行",那也可以看看,因为答案不是"他很能写",而是"他把很多该自动化的东西自动化了"。

2. Harness架构的核心分层与职责边界

2.1 为什么不能把Agent写成一个巨大的while循环

很多人第一次做Agent,写出来的东西大概是这样:一个while循环,调模型,解析输出,如果有工具调用就执行,把结果塞回上下文,继续循环,直到模型说"我完成了"。这个结构跑Demo没问题,但一旦要长期运行、要接多个工具、要处理失败重试,就会迅速失控。

Harness架构的第一个核心思想就是分层。我自己的项目里,至少分成五层:

  • 模型接入层:负责和不同模型API打交道,处理重试、限流、token计数、流式输出。这一层不关心业务,只关心"把请求发出去,把响应拿回来"。
  • 上下文管理层:负责组装每次请求的上下文,包括系统提示、工具定义、历史消息、检索到的知识片段。这一层决定了token消耗的大头。
  • 工具执行层:负责注册工具、校验参数、执行工具、捕获异常、格式化返回结果。每个工具都是一个独立的模块,有自己的schema和错误处理。
  • 状态与持久化层:负责保存会话状态、任务进度、中间产物。Agent不是无状态的,它需要记住"我做到哪一步了"。
  • 编排层:负责决定下一步做什么,是继续调工具、还是切换任务、还是等待用户输入。这一层是Harness的"大脑"。

分层的价值在于:当你想换模型时,只动第一层;当你想优化成本时,只动第二层;当你想加工具时,只动第三层。如果不分层,改任何一处都可能引发连锁反应。

2.2 工具定义才是真正的工作量所在

20万行代码里,我估计有将近一半是工具定义和相关处理逻辑。为什么这么多?因为一个"能用"的工具和一个"好用"的工具,差距巨大。

举个具体的例子。假设你要做一个"把Markdown表格转换成Excel"的工具。最简版本可能是:接收Markdown文本,解析表格,输出xlsx文件。但实际项目里,你需要考虑:

  • 表格里有没有合并单元格?Markdown本身不支持合并,但用户可能用HTML标签写。
  • 表格单元格里有没有换行?Markdown表格的换行处理是个经典坑,不同解析器行为不一致。
  • 表格前后有没有其他内容?要不要保留?
  • 输出Excel时,列宽怎么定?表头要不要加粗?数字要不要识别成数值类型?
  • 如果表格特别大,要不要分sheet?
  • 如果解析失败,返回什么错误信息,让模型能理解并重试?

这些分支加起来,一个工具就是几百行。你有几十个工具,就是几万行。所以20万行不是"写得多",而是"考虑得全"。

提示:工具定义里一定要写清楚"什么时候不该用这个工具"。模型经常会在不合适的场景调用工具,如果你在description里明确写了边界,能减少大量无效调用,直接省token。

2.3 状态机比自由对话更可控

Harness应用和普通聊天机器人的最大区别,是它需要完成任务,而不是聊天。任务是有状态的:开始、进行中、等待输入、成功、失败、取消。如果你用自由对话的方式管理,模型很容易"忘记"自己在做什么。

我的做法是引入一个轻量状态机。每个任务有明确的状态定义和允许的转移。比如一个"知识库索引"任务:

  1. pending:任务已创建,等待开始
  2. scanning:正在扫描Obsidian目录
  3. parsing:正在解析Markdown文件
  4. embedding:正在生成向量
  5. indexing:正在写入索引
  6. done/failed

每次模型决定下一步动作时,状态机告诉它"当前在哪个状态,允许哪些操作"。这样即使对话很长,模型也不会跑偏。而且状态可以持久化到磁盘,程序重启后能恢复。

3. 每月40亿token是怎么花掉的,以及怎么省

3.1 先算清楚token都去哪了

不记账的Agent项目,成本一定失控。我在项目里加了一个token计数器,按"请求类型"分类统计。跑了一个月后,数据大概是这样的:

消耗类型占比说明
系统提示+工具定义35%每次请求都要带,是固定开销
历史对话上下文25%随对话轮次线性增长
工具返回结果20%文件内容、搜索结果等
检索到的知识片段15%RAG场景下的大头
模型实际推理输出5%真正"思考"的部分

这个分布很说明问题:真正用于推理的token只有5%,95%都是"上下文搬运"。所以省token的核心不是让模型少想,而是让上下文更精简。

3.2 系统提示和工具定义的压缩策略

系统提示和工具定义是每次请求的固定开销。如果你有50个工具,每个工具定义平均200token,那就是1万token,每次请求都要带。一天2600次请求,就是2600万token,一个月7.8亿——光工具定义就烧掉这么多。

压缩策略有几个:

  • 工具分组:不是所有任务都需要所有工具。把工具按场景分组,根据当前任务只加载相关组的定义。比如"文档处理"任务只加载Markdown相关工具,"代码分析"任务只加载代码相关工具。这一招能砍掉60%以上的工具定义开销。
  • 定义精简:工具description不要写小作文,写清楚"做什么、什么时候用、关键参数"就行。参数description同理。我见过有人给每个参数写三行说明,完全没必要。
  • 动态加载:对于不常用的工具,可以先只给模型一个"工具目录"(工具名+一句话说明),模型需要时再加载完整定义。这叫"渐进式工具披露"。

3.3 历史上下文的滑动窗口与摘要

历史对话是另一个大头。如果每轮都带完整历史,10轮之后上下文就爆炸了。我的做法是滑动窗口+摘要:

  • 保留最近N轮完整对话(N根据任务复杂度定,一般5-10轮)
  • 更早的对话压缩成一段摘要,由模型自己生成
  • 关键信息(如用户偏好、任务目标、已确认的决策)单独提取出来,放在系统提示里,不随窗口滑动

这样上下文长度基本恒定,不会随对话轮次增长。实测下来,长对话场景能省70%以上的历史token。

3.4 工具返回结果的截断与结构化

工具返回结果经常很大。比如读一个Markdown文件,可能几千token;搜索一次知识库,返回十几个片段,又是几千token。如果原样塞回上下文,很快就满了。

处理原则是:只返回模型需要的信息,而不是全部信息。

  • 读文件时,如果文件很长,先返回前若干行+总行数+结构摘要,模型需要更多再分段读。
  • 搜索结果按相关度排序,只返回top K,每个片段截断到合理长度。
  • 结构化数据(如JSON)只返回关键字段,不要整个对象dump进去。

这些处理都要在工具执行层做,而不是让模型自己处理。模型处理大文本的能力有限,而且很贵。

3.5 缓存能省的钱比你想的多

很多模型API支持prompt caching,对于重复的前缀(如系统提示、工具定义)可以缓存,命中缓存的部分按更低价计费。Harness应用的系统提示和工具定义基本不变,非常适合缓存。

要利用好缓存,关键是保持前缀稳定。也就是说,系统提示和工具定义的顺序、内容不要频繁变动。如果你每次请求都动态调整工具顺序,缓存就失效了。我的做法是把稳定部分放在最前面,动态部分(如当前任务状态)放在后面。

4. 和Obsidian知识库打通的实际做法

4.1 为什么选Obsidian作为知识底座

Obsidian的核心优势是本地Markdown文件+双向链接。所有笔记都是纯文本,存在本地文件夹里,格式开放,程序可以直接读写。对于Agent应用来说,这意味着:

  • 不需要通过API访问知识库,直接读文件系统就行,速度快、无限制。
  • Markdown格式天然适合大模型处理,不需要额外的格式转换。
  • 双向链接([[笔记名]])提供了现成的知识图谱结构,可以用来做检索增强。

我用Obsidian管理项目文档、技术笔记、会议记录,然后让Agent直接在这个知识库上工作。比如"帮我找一下上次关于token优化的讨论",Agent就去搜索相关笔记,读取内容,总结回答。

4.2 目录结构与索引策略

Obsidian库的目录结构直接影响检索效率。我的建议是:

vault/ daily/ # 日记,按日期 projects/ # 项目文档 notes/ # 技术笔记 templates/ # 模板 attachments/ # 附件 .index/ # 索引文件(Agent生成)

索引策略上,我做了两层:

  • 元数据索引:扫描所有Markdown文件,提取标题、标签、链接、修改时间,存成一个JSON或SQLite。这层很轻量,可以频繁重建。
  • 向量索引:对文件内容分块,生成向量,存到向量库。这层比较重,只在内容变化时增量更新。

检索时先用元数据索引快速缩小范围(比如"只看projects目录下最近修改的文件"),再用向量索引做语义匹配。两层结合,既快又准。

4.3 Markdown解析的坑比想象中多

Markdown看起来简单,但解析起来坑很多。我在项目里踩过的:

  • 换行处理:Markdown里单个换行默认不产生新段落,但很多用户以为会。不同解析器(CommonMark、GFM)行为不一致。处理时要么统一用双换行分段,要么在解析时把单换行转成<br>。
  • 表格解析:表格的列对齐、单元格内管道符转义、表格前后空行要求,都是坑。特别是单元格里如果有|,必须转义成\|,否则解析错位。
  • 代码块嵌套:代码块里如果有三个反引号,会提前结束代码块。要用四个反引号包裹。
  • 链接和图片:相对路径、绝对路径、URL编码,处理起来很繁琐。
  • Frontmatter:Obsidian用YAML frontmatter存元数据,解析时要单独处理,不能当正文。

我的做法是:不自己写解析器,用成熟的库。Python用markdown-it-py或mistune,JavaScript用markdown-it或remark。但即使是用库,也要写一层封装,处理Obsidian特有的语法(如[[wikilink]]、![[embed]])。

4.4 双向链接的利用

Obsidian的双向链接是宝藏。[[笔记A]]表示当前笔记链接到笔记A,反向链接就是所有链接到当前笔记的笔记。Agent可以利用这个结构做:

  • 相关笔记推荐:找到当前笔记的所有出链和入链,作为相关上下文。
  • 知识图谱遍历:从一个概念出发,沿着链接走N跳,收集相关概念。
  • 孤立笔记检测:找出没有任何链接的笔记,提示用户整理。

实现上,解析所有文件的链接,构建一个有向图,然后用图算法处理。这部分代码不多,但效果很好。

5. Claude Code和DeepSeek Harness的集成经验

5.1 Claude Code适合做什么,不适合做什么

Claude Code是一个终端里的Agent工具,强项是代码理解和文件操作。它能读代码、改代码、跑命令、看输出,形成一个闭环。在我的项目里,我用它做:

  • 代码审查和重构建议
  • 写测试用例
  • 排查bug(给它错误信息,让它找原因)
  • 生成文档

但它不适合做长时间运行的后台任务。Claude Code是交互式的,你给它一个任务,它做完就结束。如果你要跑一个持续几小时的知识库索引任务,用它就不合适,应该用自己写的Harness应用。

5.2 DeepSeek Harness的插件机制

DeepSeek Harness提供了插件机制,可以注册自定义工具(Skill)。这和我自己搭Harness的思路是一致的,只是它提供了现成的框架。用它的好处是省去了模型接入、上下文管理这些基础设施,你只需要写业务工具。

我写了一个"Obsidian工具包"插件,包含:

  • search_notes:按关键词或语义搜索笔记
  • read_note:读取指定笔记内容
  • write_note:创建或更新笔记
  • list_notes:列出目录下的笔记
  • get_backlinks:获取反向链接

每个工具就是一个函数,加上schema定义。Harness负责调用和结果处理。这样我不用关心模型怎么调工具,只关心工具本身。

5.3 多模型切换的实际考虑

项目里我同时用了几个模型:Claude做复杂推理和代码任务,DeepSeek做中文处理和成本敏感的任务,本地小模型做简单的分类和提取。切换逻辑在模型接入层,根据任务类型路由。

切换时要注意:

  • 提示词要适配:不同模型对提示词的敏感度不同。Claude对结构化提示响应好,DeepSeek对中文指令理解好。系统提示要针对模型微调。
  • 工具调用格式:不同模型的工具调用格式可能不同(有的用JSON,有的用特定标记)。接入层要做归一化。
  • 错误处理:不同模型的错误码和限流策略不同,重试逻辑要分别处理。

6. 那些只有踩过才知道的坑

6.1 Agent执行中断的错误处理

热词里有个"agent execution terminated due to error",这是Agent开发中最常见的问题之一。Agent跑到一半挂了,可能是模型返回格式错误、工具执行异常、网络超时、上下文超长。

我的处理原则是:任何一步失败都不能让整个任务崩溃。

  • 模型返回格式错误:重试,最多3次,每次在提示里加上"上次返回格式错误,请严格按JSON格式返回"。
  • 工具执行异常:捕获异常,把错误信息格式化后返回给模型,让模型决定是重试还是换方法。
  • 网络超时:指数退避重试。
  • 上下文超长:触发压缩逻辑,摘要历史后重试。

关键是状态要持久化。任务执行到哪一步、已经产出了什么,都要存盘。这样即使进程挂了,重启后能从断点继续,而不是从头再来。

6.2 Markdown转Word的序号问题

热词里有"dify markdown转word中序号自动编号",这是个很具体的坑。Markdown的有序列表是1. 2. 3.,转成Word后期望是自动编号,但很多转换工具只是把数字当文本,导致序号是"死"的,增删条目不会自动调整。

解决方案是:转换时识别有序列表,生成Word的编号列表(numbering),而不是纯文本。如果用python-docx,需要操作numbering.xml,比较麻烦。更简单的做法是用pandoc,它处理得比较好。如果一定要自己写,就要在解析Markdown时标记列表层级,生成对应的Word样式。

6.3 Obsidian Git同步的冲突

用Obsidian Git做版本管理很方便,但多设备同步时容易冲突。Agent如果也在写文件,冲突概率更高。我的做法是:

  • Agent写文件前先pull,写完立即commit+push。
  • 给Agent写的文件加特定前缀或放在特定目录,减少和手动编辑的冲突。
  • 冲突时以Agent版本为准(因为Agent是基于最新内容生成的),但保留手动版本到.conflict文件。

6.4 上下文超长的隐蔽原因

有时候上下文莫名其妙就超了,排查半天发现是某个工具返回了巨大结果。比如搜索工具返回了100个片段,每个片段1000token,一次就是10万token。所以每个工具都要有输出大小限制,超过就截断,并在返回里说明"结果已截断,共X条,显示前Y条"。

另一个隐蔽原因是递归调用。Agent调工具A,工具A内部又调了Agent,Agent又调工具A……无限递归。要在工具执行层加调用深度限制。

7. 一个人维护20万行代码的工程习惯

7.1 模块化到"每个工具一个文件"

20万行代码如果堆在几个文件里,根本没法维护。我的做法是每个工具一个文件,文件名就是工具名。这样找代码、改代码、加工具都很清晰。工具之间通过统一的接口注册,不直接互相引用。

目录结构大概是这样:

src/ core/ # 框架核心 model/ # 模型接入 context/ # 上下文管理 tools/ # 工具注册与执行 state/ # 状态管理 tools/ # 具体工具 search_notes.py read_note.py markdown_to_excel.py ... prompts/ # 提示词模板 config/ # 配置 tests/ # 测试

7.2 测试是省时间的,不是花时间的

一个人做项目,最容易省的就是测试。但Agent项目恰恰最需要测试,因为行为不确定。我的测试分三层:

  • 单元测试:每个工具单独测,输入输出明确。
  • 集成测试:模拟一次完整的Agent任务,检查最终结果。
  • 回归测试:把踩过的坑都写成测试用例,防止改代码时重新踩。

特别是提示词改动,一定要跑回归测试。改一句系统提示,可能让模型行为大变。有测试兜底,才敢改。

7.3 日志要记到"能复现问题"的程度

Agent出问题时,如果没有详细日志,根本没法排查。我的日志记录:

  • 每次模型请求的完整上下文(脱敏后)
  • 模型的完整响应
  • 每次工具调用的参数和结果
  • 状态转移
  • token消耗

日志按天分文件,保留30天。出问题时,找到对应的请求ID,就能完整复现当时的场景。

7.4 配置和代码分离

模型选择、API地址、token限制、工具开关这些,全部放配置文件,不硬编码。这样换环境、调参数不用改代码。配置文件用YAML,支持环境变量覆盖。

8. 成本控制的几个反直觉结论

8.1 用更贵的模型可能更省钱

听起来矛盾,但实际是这样:如果一个任务用便宜模型要5轮才做对,用贵模型1轮就做对,那贵模型可能更省。因为每轮都要带上下文,5轮的上下文成本可能超过1轮用贵模型的成本。

所以我的策略是:简单任务用便宜模型,复杂任务直接用最好的模型,不要在复杂任务上省钱,省出来的钱会被重试吃掉。

8.2 减少工具数量比优化工具定义更有效

前面说过工具定义占35%的token。与其花时间精简每个工具的定义,不如直接减少工具数量。很多工具其实可以合并,或者用参数区分。工具越少,模型选择越准,token越省。

8.3 缓存命中率是最大的成本杠杆

prompt caching的命中部分价格可能只有未命中的十分之一。如果你的系统提示和工具定义稳定,命中率能到80%以上,成本直接砍半。所以保持前缀稳定这件事,比任何优化都值钱。

8.4 不是所有任务都需要Agent

有些任务用传统程序就能做,不需要Agent。比如"把Markdown表格转Excel",写个脚本就行,不需要模型参与。Agent应该用在需要理解、判断、生成的任务上。把不需要Agent的任务交给Agent,是最大的浪费。

9. 后续可以继续深挖的方向

这套Harness应用跑下来,我觉得还有几个方向值得继续做:

  • 多Agent协作:一个Agent负责规划,一个负责执行,一个负责检查。分工明确后,每个Agent的上下文可以更精简。
  • 本地模型替代:对于分类、提取这类简单任务,用本地小模型替代API调用,能省不少钱,而且没有网络延迟。
  • 知识库自动整理:让Agent定期扫描Obsidian库,发现重复笔记、失效链接、孤立笔记,自动整理或提示。
  • 提示词版本管理:把提示词当代码管理,每次改动记录效果,找到最优版本。

这些方向我还在摸索,有进展再分享。如果你也在做类似的项目,欢迎交流踩坑经验——毕竟一个人做项目,最大的成本不是token,是没人告诉你前面有个坑。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询