news 2026/10/1 12:58:23

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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 架构,而不是直接调 API

2.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-bm25

Claude Code 的安装按官方文档走就行,装完之后用claude --version验证。如果遇到组织策略限制导致无法使用的情况,那就换用其他兼容的模型接口,Harness 层的适配器设计本来就是为了应对这种情况。

4.2 知识库索引的构建

索引构建是个离线任务,可以定时跑,也可以手动触发。流程分四步:扫描文件、解析内容、切块、写入索引。

扫描文件时要注意排除.obsidian目录和附件目录,只处理.md文件。解析用前面说的 Markdown 解析库,把每个文件解析成 AST,然后遍历 AST 提取文本块和元数据。

切块逻辑我写过一个简化版,核心思路是:

def chunk_by_heading(ast, max_len=800): 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_steps=15): 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 应用的人应该都有共鸣。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/1 12:58:17

GitHub日榜项目筛选与实操:从趋势洞察到工具链改造

1. 日榜项目的筛选逻辑与信息价值 1.1 为什么日榜比周榜更有参考意义 GitHub 热榜的日榜和周榜、月榜看起来只是时间窗口不同,但实际用起来差别很大。日榜反映的是“过去24小时内新增 star 速度最快”的项目,这个指标对开发者来说更敏感,因为…

作者头像 李华
网站建设 2026/10/1 12:57:34

MindSpore大模型训练迁移:transformer_config配置解析与实战

1. 大模型训练迁移这件事,为什么绕不开 transformer_config做过大模型训练的人都有一个共识:换框架比换模型难。模型结构是公开的,权重是可以转换的,但训练框架里那一套配置体系、并行策略、优化器行为、混合精度处理方式&#xf…

作者头像 李华
网站建设 2026/10/1 12:56:41

Win10局域网键鼠共享:Mouse without Borders从配置到排错全攻略

好的,我来写这篇博文。主题很明确:win10 下用 Mouse without Borders 做局域网键鼠共享。我会从实际工作场景切入,讲清楚选型思路、完整配置流程、核心功能实测、常见问题排查,最后对比同类方案。全程用从业者交流的口吻&#xff…

作者头像 李华
网站建设 2026/10/1 12:56:16

NVMe CMB与DMA-BUF:内核设备内存共享接口之争

1. 从"第1个NVMe硬盘的第5个分区"说到控制器里的那小块神秘内存 最近后台总能刷到这么几个搜索词: /dev/nvme0n1p5 是第 1 个 NVMe 硬盘的第 5 个分区吗、E5 平台配 NVMe 跑 Win10 启动一般要多久、用 NTLite 往老镜像里塞 USB3.0 和 NVMe 驱动、NVMe 固…

作者头像 李华
网站建设 2026/10/1 12:55:53

01子序列构造题详解:HJ117的数学推导与代码实现

做了这么多年的算法题,我对“构造”这类题目一直保持警惕:它不像普通模拟题那样照流程跑一遍就行,也不像DP题靠状态转移推到底,而是要先读懂题目想让你构造什么,再用数学规律把答案“算”出来。HJ117“小红的01子序列构…

作者头像 李华
网站建设 2026/10/1 12:55:09

eNSP连线字典:端口类型匹配、线缆选择与接口UP排查

1. 把线和口的关系先摆正:eNSP连不连得通,物理层说了算 带过几批新人之后我发现一个特别稳定的规律:拓扑搭不起来,八成不是命令敲错,而是线缆和端口压根没对上。有人在两台交换机之间拉一根串口线,有人拿着…

作者头像 李华