ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Harness架构实战:九个月二十万行代码构建知识管理Agent

Harness架构实战:九个月二十万行代码构建知识管理Agent 1. 一个人九个月二十万行代码这件事到底在做什么先把标题里的几个数字拆开看。一个人意味着没有团队分工没有前后端联调没有产品经理帮你砍需求所有决策链路都压在一个人的工作台上。九个月大约是 270 天如果按每周工作六天算实际投入大概在 230 个工作日左右。20 万行代码摊到每个工作日大约是 870 行有效代码——注意是有效代码不是那种复制粘贴凑数的行数。每个月烧掉 40 亿以上的 token这个量级放在个人开发者身上是相当夸张的它意味着这个项目里有大量由模型驱动的自动化环节而不是单纯把模型当个聊天窗口用。这个项目做出来的东西是一款Harness 架构应用。Harness 这个词在当下的语境里指的是一套把大模型能力套住、让它按照既定流程稳定干活的工程骨架。你可以把它理解成给一匹力气很大但脾气不定的马配上一整套缰绳、鞍具和路线图。模型本身是那匹马Harness 就是让它能拉车、能走直线、能在指定路口转弯的那套装备。没有 Harness模型只能陪你聊天有了 Harness它才能变成一个真正能干活的 Agent。那这款应用具体解决什么问题从关键词的组合来看它围绕的是知识管理 智能体执行这条线。Markdown 是内容载体Obsidian 是知识库的落地形态Claude Code 和 DeepSeek Harness 是执行引擎Agent 是最终呈现给用户的能力形态。说白了它想做的事情是让一个人对着自己的笔记库说一句话背后有一整套 Agent 体系去读笔记、理解结构、执行操作、回写结果整个过程不需要人手动去点几十个菜单。适合谁来参考这篇内容三类人。第一类是自己有大量 Markdown 笔记、想用 Agent 把笔记盘活的知识工作者第二类是在做 Agent 应用、想知道别人怎么把架构搭起来的开发者第三类是对 Harness 这个概念还比较模糊、想搞清楚它和普通调 API有什么区别的技术人。不管你是哪一类接下来的内容都会从架构思路一路讲到实操细节和踩坑记录。2. 为什么是 Harness 架构而不是直接调 API2.1 直接调 API 的三个死穴很多人做 Agent 的第一反应是不就是调个模型 API把用户输入塞进去把输出拿出来吗这个思路在 demo 阶段没问题但一旦要长期跑、要处理复杂任务马上会撞上三堵墙。第一堵墙是上下文管理。模型的上下文窗口是有限的而一个真实的知识库可能有几千篇笔记。你不可能每次都把整个库塞进去必须有一套机制去决定这次任务该读哪些笔记、读多少、按什么顺序读。这套机制就是 Harness 的核心职责之一。第二堵墙是执行可靠性。模型输出的东西不一定是你要的格式。它可能今天返回 JSON明天返回一段带解释的自然语言后天在 JSON 外面裹一层 Markdown 代码块。如果没有一层解析和校验你的下游代码会天天崩。Harness 要做的就是把这层不确定性吃掉对外暴露稳定的接口。第三堵墙是多步任务的编排。一个把这篇笔记里的待办事项提取出来按优先级排序然后同步到另一个文件的任务拆开看至少有四步读文件、提取、排序、写文件。每一步都可能失败每一步都需要重试策略。裸调 API 的话这些逻辑全得你自己写写着写着就变成一团乱麻。2.2 Harness 到底套住了什么Harness 架构的本质是在模型和业务逻辑之间插入一个中间层。这个中间层干三件事约束输入、规范输出、编排流程。约束输入指的是它负责组装每次送给模型的 prompt。这里面包括系统提示词、当前任务描述、从知识库里检索出来的相关片段、以及历史对话的摘要。组装策略直接决定了模型能不能拿到足够的信息又不会因为塞太多而迷失重点。规范输出指的是它定义了一套模型必须遵守的输出协议。常见做法是要求模型输出结构化数据比如带特定字段的 JSON或者带特定标记的文本块。Harness 拿到输出后先做解析解析失败就触发重试重试还失败就降级处理。这样上层业务代码永远拿到的是干净的数据。编排流程指的是它把一个大任务拆成若干步骤每一步的输入输出都经过 Harness 中转。哪一步失败了Harness 知道该重试还是该跳过还是该报错。这套编排逻辑通常用一个状态机或者任务图来描述而不是一堆 if-else 堆出来的。2.3 为什么这个项目选了 Harness 而不是别的从关键词里能看到 Claude Code 和 DeepSeek Harness 同时出现说明这个项目大概率是多引擎并存的。这本身就是选 Harness 架构的一个强理由当你需要同时对接多个模型供应商时如果每个供应商的调用逻辑都散落在业务代码里改一处就要动全身。把它们统一收口到 Harness 层业务层只跟 Harness 打交道换引擎就是换一个适配器的事。另一个理由是可观测性。每个月 40 亿 token 的消耗如果不做精细的埋点和统计你根本不知道钱花在哪了。Harness 层天然是所有请求的必经之路在这里记录每次调用的输入长度、输出长度、耗时、成功与否就能生成非常清晰的成本报表。哪些任务最费 token、哪个环节重试率最高一目了然。还有一点是可测试性。Harness 把模型调用抽象成了一个接口你可以在测试时用一个假的实现替换掉真实模型返回预设的输出这样就能在不花钱的情况下测试整个流程的逻辑。没有这层抽象测试 Agent 应用会非常痛苦。3. 核心模块拆解与关键实现细节3.1 知识库读取层Markdown 解析没那么简单这个项目的知识库是 Obsidian 形态的也就是一堆 Markdown 文件加双链。读取层要做的第一件事是把 Markdown 解析成结构化数据。听起来简单实际上坑很多。Markdown 的语法看似标准但各家实现都有差异。比如表格标准 Markdown 表格要求表头下面有一行分隔符但很多笔记软件允许省略。再比如数学公式有的是$...$行内有的是$$...$$块级还有的用\(...\)。解析器如果只认一种就会漏掉内容。实操上我建议用成熟的 Markdown 解析库比如 JavaScript 生态里的remark系列或者 Python 生态里的markdown-it-py。这些库通常支持插件机制你可以针对 Obsidian 的特有语法比如[[双链]]、![[嵌入]]、 [!note]这种 callout写自定义插件。解析完之后要把内容切成适合检索的块。切块策略直接影响检索质量。切太碎语义不完整切太大检索精度下降。一个比较稳的做法是按标题层级切每个二级标题下的内容作为一个块如果这个块太长比如超过 800 字再按段落二次切分。这样每个块都有明确的主题检索时更容易命中。注意Obsidian 的双链在解析时要特殊处理。[[某篇笔记]]这种链接如果直接当普通文本处理检索时用户搜某篇笔记是搜不到的。正确做法是把链接目标也提取出来作为这个块的元数据存进去。3.2 检索层向量检索和关键词检索要混着用知识库大了之后光靠把全部内容塞给模型是不现实的。必须有检索。检索方案主要有两种向量检索和关键词检索。向量检索擅长语义匹配。用户问怎么处理时间冲突即使笔记里写的是日程重叠的解决方案向量检索也能找到。但它的弱点是精确匹配差用户搜一个特定的函数名或者人名向量检索可能返回一堆语义相近但不对的结果。关键词检索比如 BM25正好相反精确匹配强语义泛化弱。这个项目里比较合理的做法是混合检索两路都跑然后做结果融合。融合算法可以用 Reciprocal Rank Fusion简单说就是把两路结果的排名做加权求和排名靠前的权重高。这个算法不需要调参效果稳定适合作为默认方案。检索出来的结果不能直接全塞给模型还要做重排序。可以用一个小的交叉编码器模型对候选结果打分取 top-k。如果不想引入额外模型也可以用规则重排比如标题匹配的加权、最近修改的加权。3.3 Agent 执行层任务怎么拆、怎么串Agent 执行层是整个 Harness 的心脏。它要解决的问题是用户给一个自然语言指令怎么把它变成一系列可执行的操作。常见做法是规划-执行两段式。第一段让模型做规划输出一个步骤列表每个步骤包含操作类型和参数。第二段逐步执行每执行完一步把结果反馈给模型让模型决定下一步是继续、调整还是终止。这里有个关键设计步骤的粒度。粒度太粗模型容易一步做太多导致出错粒度太细步骤数量爆炸token 消耗飙升。经验值是每个步骤对应一个原子操作比如读取文件 A、在文件 A 中查找包含关键词 X 的段落、把结果写入文件 B。一个中等复杂度的任务步骤数控制在 5 到 15 之间比较合适。执行层还要处理错误恢复。模型规划出来的步骤不一定都能成功执行。比如它让你读一个不存在的文件这时候不能直接崩要把错误信息回传给模型让它重新规划。这就形成了一个循环规划、执行、遇错、重新规划。循环要有次数上限比如最多重试 3 次超过就报错退出避免死循环烧 token。3.4 输出回写层改笔记要小心Agent 执行完任务结果要写回知识库。这一步风险最高因为写错了可能污染用户的笔记。安全做法是先写临时文件确认无误再替换。具体来说Agent 生成的新内容先写到一个.tmp文件然后做一个 diff把变更展示给用户确认或者在某些低风险场景下自动确认确认后才覆盖原文件。同时保留一份原文件的备份万一出问题可以回滚。对于批量操作比如把所有笔记里的某个标签改名更要谨慎。建议先做一次 dry-run只输出将要变更的列表不实际写入。用户确认列表没问题再执行真正的写入。提示Obsidian 的笔记文件如果正在被 Obsidian 打开直接覆盖可能会有冲突。稳妥做法是通过 Obsidian 的本地接口如果有插件支持来操作或者提示用户先关闭 Obsidian。4. 从零搭建的完整实操流程4.1 环境准备与依赖安装先说基础环境。这个项目涉及 Node.js 生态Claude Code 相关工具链和 Python 生态很多检索和 NLP 库所以两套环境都要有。Node.js 建议用 20 以上的 LTS 版本用nvm管理版本比较方便。Python 建议 3.11 以上用venv或者conda建独立环境。数据库方面向量检索可以用本地的向量库比如chromadb或者lancedb都是嵌入式方案不需要额外起服务。# Node 环境 nvm install 20 nvm use 20 # Python 环境 python -m venv .venv source .venv/bin/activate # Windows 用 .venv\Scripts\activate # 核心依赖 pip install markdown-it-py chromadb sentence-transformers rank-bm25Claude Code 的安装按官方文档走就行装完之后用claude --version验证。如果遇到组织策略限制导致无法使用的情况那就换用其他兼容的模型接口Harness 层的适配器设计本来就是为了应对这种情况。4.2 知识库索引的构建索引构建是个离线任务可以定时跑也可以手动触发。流程分四步扫描文件、解析内容、切块、写入索引。扫描文件时要注意排除.obsidian目录和附件目录只处理.md文件。解析用前面说的 Markdown 解析库把每个文件解析成 AST然后遍历 AST 提取文本块和元数据。切块逻辑我写过一个简化版核心思路是def chunk_by_heading(ast, max_len800): chunks [] for node in ast.walk(ast): if node.type heading and node.level 2: content extract_text_until_next_h2(node) if len(content) max_len: chunks.extend(split_by_paragraph(content, max_len)) else: chunks.append({ heading: node.text, content: content, source: current_file }) return chunks写入索引时每个块要同时写入向量库和关键词索引。向量用sentence-transformers生成模型选bge-small-zh这类中文效果好的小模型就够了不需要上大模型因为检索阶段追求的是速度和够用。4.3 Harness 核心类的设计Harness 核心类我建议设计成三个部分InputBuilder、Executor、OutputParser。InputBuilder负责组装 prompt。它接收任务描述和检索结果按模板拼成最终输入。模板里要明确告诉模型你的角色是什么、当前任务是什么、可用工具列表、输出格式要求。Executor负责调用模型。它接收组装好的输入调用配置好的模型接口拿到原始输出。这里要做超时控制和重试。超时建议设 60 秒重试最多 2 次重试时可以在 prompt 里加上上次输出格式不对请严格按格式返回。OutputParser负责解析输出。如果模型返回的是 JSON先尝试直接解析失败的话用正则提取代码块里的 JSON 再解析还失败就触发重试。class Harness: def __init__(self, model_client, retriever): self.model model_client self.retriever retriever self.input_builder InputBuilder() self.parser OutputParser() def run(self, task, max_steps15): context self.retriever.search(task) prompt self.input_builder.build(task, context) for step in range(max_steps): raw self.model.call(prompt) parsed self.parser.parse(raw) if parsed.type final: return parsed.content result self.execute(parsed.action) prompt self.input_builder.append_result(prompt, result) raise MaxStepsExceeded()4.4 成本控制的实际手段每月 40 亿 token 听起来吓人但拆开看是可以优化的。主要手段有三个。缓存。同样的输入如果之前调用过直接返回缓存结果。知识库问答场景里很多问题是重复的缓存命中率能到 20% 以上。缓存 key 用输入内容的哈希存到本地 KV 库就行。分级模型。不是所有任务都需要最强的模型。规划任务用强模型执行简单操作比如格式化输出用便宜的小模型。Harness 层根据任务类型路由到不同模型能省不少钱。上下文压缩。历史对话不要全量保留超过一定轮数就做摘要。摘要用一个便宜模型生成把十轮对话压成一段话token 消耗能降一个数量级。5. 常见问题与排查实录5.1 模型输出格式不稳定怎么办这是最高频的问题。表现是同样的 prompt有时候返回纯 JSON有时候 JSON 外面裹了 json 代码块有时候还加一句好的以下是结果。排查思路分三层。第一层检查 prompt 里的格式要求是否足够明确。不要只说返回 JSON要说只返回 JSON不要有任何其他文字不要用代码块包裹。第二层检查是否有 few-shot 示例。给一两个输入输出示例格式稳定性会明显提升。第三层如果还是不稳就在解析层做容错用正则把 JSON 部分抠出来。实操心得我在 prompt 末尾加一句如果你理解了直接开始输出不要复述我的要求能减少很多模型好的我来帮你这类废话。5.2 检索结果不相关怎么调检索不准先分清是向量检索的问题还是关键词检索的问题。做法是分别看两路的结果。如果向量检索返回的都是语义相近但主题不对的可能是 embedding 模型不适合你的领域换一个在中文长文本上表现更好的模型。如果关键词检索返回的都不对检查分词是否正确中文分词用jieba这类工具别用空格切。还有一个常见原因是切块策略。如果块切得太碎每个块信息量不足检索自然不准。试着把块调大一点或者改成按语义段落切。5.3 Agent 陷入死循环怎么破死循环的典型表现是Agent 反复执行同一个操作或者反复在规划-失败-重新规划之间打转。根因通常是错误信息没有正确回传给模型。比如文件读取失败你只回传了失败模型不知道是文件不存在还是权限问题就会一直重试。正确做法是把完整的错误信息回传包括错误类型和具体描述。另一个根因是步骤上限设得太高。有些任务模型确实规划不出来这时候应该早点终止而不是让它试 50 次。步骤上限设 15 左右比较合理超过就报错让用户介入。5.4 常见问题速查表问题现象可能原因排查方向解决手段输出格式不稳定prompt 约束不足检查格式描述和示例加 few-shot强化格式要求检索结果不相关切块策略或 embedding 模型问题分别看两路检索结果调整切块粒度换 embedding 模型Agent 死循环错误信息不完整或步骤上限过高看日志里的重试记录回传完整错误降低步骤上限token 消耗异常高上下文未压缩或缓存未命中看每次调用的输入长度加缓存做上下文摘要笔记写入冲突文件被占用或并发写入检查文件锁写临时文件再替换加备份解析器报错Markdown 语法不兼容定位具体文件加自定义解析插件5.5 几个容易忽略的细节第一个细节是文件编码。中文笔记里经常混着 UTF-8 和 GBK读取时不统一处理会乱码。建议读取时先探测编码统一转成 UTF-8 再处理。第二个细节是空行和空格。Markdown 对空行敏感有些语法比如列表前面必须有空行才生效。Agent 生成内容时经常忽略这点导致写回去的笔记格式乱掉。可以在输出回写前做一次格式化用prettier这类工具统一处理。第三个细节是双链的维护。如果 Agent 重命名了一篇笔记所有指向它的双链都要更新。这个操作如果漏了知识库的链接就断了。建议在回写层加一个钩子检测到文件重命名时自动扫描全库更新双链。6. 这套架构还能怎么扩展跑通基础版本之后有几个方向可以继续深挖。一个是多模态。现在的知识库主要是文本但很多笔记里嵌了图片。如果能把图片也做 embedding检索时就能支持找那张画了流程图的笔记这种查询。图片 embedding 可以用 CLIP 类模型把图片和文本映射到同一空间。另一个是主动式 Agent。现在的模式是用户提问、Agent 响应。可以改成 Agent 定时扫描知识库发现待办事项快到期了主动提醒或者发现两篇笔记内容矛盾了主动标记。这需要一套调度机制但 Harness 层的结构不用大改加一个定时触发器就行。还有一个是协作。如果多个人共用一个知识库Agent 的操作需要区分权限。谁可以改哪些目录谁只能读这些规则可以在 Harness 层做拦截。实现上就是在执行操作前加一层权限检查不通过就拒绝执行并返回原因。我个人在实际操作中的体会是Harness 架构最大的价值不在于它让模型变聪明了而在于它让模型的行为变得可预测。一个可预测的系统哪怕能力上限低一点也比一个时好时坏的系统好用得多。九个月 20 万行代码大部分工作量其实都花在把各种边界情况处理干净上真正调模型的代码可能连十分之一都不到。这个比例做过 Agent 应用的人应该都有共鸣。
返回列表