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应用和普通聊天机器人的最大区别是它需要完成任务而不是聊天。任务是有状态的开始、进行中、等待输入、成功、失败、取消。如果你用自由对话的方式管理模型很容易忘记自己在做什么。我的做法是引入一个轻量状态机。每个任务有明确的状态定义和允许的转移。比如一个知识库索引任务pending任务已创建等待开始scanning正在扫描Obsidian目录parsing正在解析Markdown文件embedding正在生成向量indexing正在写入索引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编码处理起来很繁琐。FrontmatterObsidian用YAML frontmatter存元数据解析时要单独处理不能当正文。我的做法是不自己写解析器用成熟的库。Python用markdown-it-py或mistuneJavaScript用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写完立即commitpush。给Agent写的文件加特定前缀或放在特定目录减少和手动编辑的冲突。冲突时以Agent版本为准因为Agent是基于最新内容生成的但保留手动版本到.conflict文件。6.4 上下文超长的隐蔽原因有时候上下文莫名其妙就超了排查半天发现是某个工具返回了巨大结果。比如搜索工具返回了100个片段每个片段1000token一次就是10万token。所以每个工具都要有输出大小限制超过就截断并在返回里说明结果已截断共X条显示前Y条。另一个隐蔽原因是递归调用。Agent调工具A工具A内部又调了AgentAgent又调工具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是没人告诉你前面有个坑。