ARTICLE DETAIL

资讯详情

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

Harness架构实战:一个人九个月20万行代码的Agent编排与工程化经验

Harness架构实战:一个人九个月20万行代码的Agent编排与工程化经验 1. 先搞清楚这个项目到底在做什么一个人九个月20万行代码每个月消耗40亿以上的token最终交付的是一款基于Harness架构的应用。这组数字放在任何一个技术社区里都足够炸裂但真正值得拆解的不是数字本身而是这些数字背后隐藏的工程决策链条。我第一次看到这个项目描述的时候脑子里冒出来的第一个问题是为什么要用Harness架构第二个问题是40亿token到底花在了什么地方第三个问题是一个人怎么扛住20万行代码的维护复杂度先把概念理清楚。Harness在这里指的是一种以Agent编排为核心的工程架构范式它的核心思想是把大模型的能力封装成可调度、可组合、可观测的执行单元然后通过一层编排引擎把这些单元串联成完整的业务流程。你可以把它理解成一个“AI能力总线”——每个Agent是一个独立的功能模块Harness负责决定什么时候调用哪个Agent、传什么参数、拿到结果之后怎么处理。这和传统的微服务架构有相似之处但区别在于Harness架构中的“服务”本身具有不确定性和自主决策能力这就让编排层的设计变得极其关键。这个项目选择Harness架构而不是简单的Prompt链或者单Agent方案背后的逻辑其实很直接当你的应用需要处理多种类型的任务比如文档解析、代码生成、知识检索、格式转换单Agent方案会迅速遇到上下文窗口瓶颈和能力冲突问题。一个Agent既要懂Markdown语法又要会调Claude Code的API还要能操作Obsidian的库文件这种“全栈Agent”在实际运行中会变得极其不稳定。Harness架构通过拆分职责解决了这个问题每个Agent只负责一个垂直领域编排层负责路由和状态管理。适合关注这个项目的人其实很广如果你正在做AI Agent开发这里面的架构决策和踩坑经验可以直接复用如果你在用Claude Code或者DeepSeek Harness做日常开发可以了解怎么把这些工具串联成自动化流水线如果你只是对“一个人怎么用AI工具完成大规模项目”感兴趣这里面的工作流设计思路同样有参考价值。不管你是刚接触Agent开发的新手还是已经在做多Agent编排的老手这个项目里都有值得挖的东西。2. 架构选型的底层逻辑与关键取舍2.1 为什么是Harness而不是其他方案市面上做Agent编排的方案大致分三类第一类是以LangChain为代表的重框架路线第二类是以Claude Code为代表的CLI工具路线第三类就是Harness这种轻编排层路线。这三者的核心区别在于控制粒度和调试友好度。LangChain的问题在于抽象层太厚。你写一个简单的文档处理流程可能要经过Chain、Agent、Tool、Memory四层抽象每一层都有自己的配置和生命周期。好处是功能全坏处是出了问题你根本不知道是哪一层的锅。我在实际项目里用过LangChain做知识库问答调试一个检索失败的问题花了整整两天最后发现是Memory模块的默认配置把上下文截断了。Claude Code这类CLI工具的优势是开箱即用你不需要自己搭编排层直接写Prompt就能跑。但它的局限也很明显你很难把多个CLI工具的调用串联成一个自动化流程而且每次调用都是独立的会话状态管理基本靠手动传递。对于这个项目来说20万行代码的规模意味着每天可能有上百次Agent调用纯靠CLI工具手动操作根本不现实。Harness架构的定位正好在两者之间。它不像LangChain那么重核心就是一个编排引擎加一组Agent注册接口也不像CLI工具那么轻它提供了状态管理、错误重试、日志追踪这些生产级功能。用一句话概括Harness让你用最小的抽象成本获得最大的编排灵活性。2.2 40亿token到底花在哪了很多人看到“每月40亿token”第一反应是“这也太烧钱了”但如果你拆开来看这个数字其实很合理。假设每天有200次Agent调用每次调用平均消耗6万token包括输入上下文和输出结果那一天就是1200万token一个月30天就是3.6亿。再加上一些批量处理任务比如对整个代码库做静态分析、生成文档很容易就冲到40亿。关键不在于花了多少token而在于这些token的投入产出比。我算过一笔账如果用传统方式写20万行代码一个高级工程师大概需要18到24个月按市场价算人力成本至少在百万级别。而40亿token按当前主流模型的定价成本大概在几万到十几万之间。这个账算下来token消耗反而是整个项目里最便宜的部分。真正贵的是时间。一个人九个月完成20万行代码意味着平均每天要产出700多行有效代码。这个速度靠手写是不可能的必须依赖Agent自动化生成加人工审核的流水线。所以40亿token的本质是“用机器时间换人类时间”这个交易在大多数场景下都是划算的。2.3 单Agent、多Agent和Harness的边界在哪这里有一个很容易踩的坑很多人一上来就搞多Agent结果发现协调成本比收益还大。我的经验是判断标准很简单——如果你的任务可以用一个Prompt加几个工具函数解决就别上多Agent如果任务涉及三个以上不同领域的知识且这些知识之间有明确的先后依赖关系那才考虑Harness架构。具体到这个项目它至少涉及四个垂直领域Markdown文档处理、代码生成与审查、知识库管理Obsidian、以及Agent编排本身。这四个领域的知识几乎没有重叠一个Agent不可能同时精通。所以拆成四个独立Agent由Harness统一调度是合理的架构决策。但拆分的粒度也很讲究。拆得太细Agent之间的通信开销会吃掉大部分性能拆得太粗又回到了单Agent的老问题。这个项目最终选择了“按领域拆分、按流程编排”的策略每个Agent内部可以有自己的子流程但对外只暴露一个统一的调用接口。这种设计在保持灵活性的同时控制了复杂度。3. 核心模块的实操细节与避坑指南3.1 Markdown处理管线的搭建要点Markdown在这个项目里扮演的是“中间表示层”的角色。Agent生成的代码、文档、配置全部先转成Markdown格式然后再由下游模块消费。这样做的好处是Markdown的解析和生成都有成熟的库支持而且人类可读方便调试。但Markdown处理有几个经典的坑。第一个是换行问题。不同平台对Markdown换行的处理不一致有的把单个换行当空格有的当新段落。在Agent生成内容时如果不显式控制换行符很容易出现格式错乱。我的做法是在Agent的输出模板里强制使用双换行表示段落分隔单换行只在列表项内部使用。第二个是表格转换。项目里经常需要把Markdown表格转成Excel或者从Excel转回来。这里推荐用Python的tabulate库做Markdown表格的解析用openpyxl做Excel的读写。注意Markdown表格不支持合并单元格如果你的数据有合并需求要么在转换前拍平要么在Excel侧做后处理。第三个是数学公式。Markdown本身不支持数学公式需要依赖LaTeX扩展。在Agent生成包含公式的内容时要确保公式被$...$或$$...$$包裹并且下游的渲染器支持MathJax或KaTeX。我踩过的坑是Agent有时候会把公式写成普通文本导致渲染出来是一堆乱码。解决办法是在Agent的System Prompt里明确要求“所有数学表达式必须用LaTeX语法包裹”。import re from tabulate import tabulate def markdown_table_to_list(md_text): 将Markdown表格转换为二维列表 lines [l for l in md_text.split(\n) if l.strip().startswith(|)] if len(lines) 2: return [] # 跳过分隔行 data_lines [lines[0]] lines[2:] rows [] for line in data_lines: cells [c.strip() for c in line.strip(|).split(|)] rows.append(cells) return rows def list_to_markdown_table(data, headersNone): 将二维列表转换为Markdown表格 return tabulate(data, headersheaders, tablefmtpipe)3.2 Agent编排引擎的设计与实现编排引擎是整个Harness架构的心脏。它的核心职责有三个任务路由、状态管理、错误处理。任务路由决定了一个请求应该发给哪个Agent状态管理确保多轮对话中的上下文不丢失错误处理保证单个Agent失败不会导致整个流程崩溃。任务路由最简单的实现是基于规则的if-else但这种方式在Agent数量超过五个之后会变得难以维护。更好的做法是用一个轻量级的分类器可以是一个小模型也可以是一组关键词匹配规则。这个项目用的是“关键词匹配加置信度打分”的方案每个Agent注册自己的触发关键词和优先级编排引擎根据输入内容计算每个Agent的匹配分数选分数最高的执行。状态管理是另一个容易出问题的地方。Agent之间的状态传递如果设计不好会出现“上下文爆炸”——每个Agent都往上下文里塞东西最后token全花在传递历史记录上了。我的建议是采用“最小必要状态”原则每个Agent只接收完成当前任务所需的最小上下文执行完毕后只返回结果不返回中间过程。中间过程写到日志里需要的时候再查。错误处理方面重试机制是必须的但重试策略要分情况。对于网络超时这类瞬时错误直接重试三次对于参数错误这类逻辑错误重试没有意义应该直接返回错误信息让上游处理对于模型输出格式错误可以尝试用修正Prompt重新调用一次。class HarnessEngine: def __init__(self): self.agents {} self.state_store {} def register_agent(self, name, agent, keywords, priority0): self.agents[name] { instance: agent, keywords: keywords, priority: priority } def route(self, user_input): scores {} for name, config in self.agents.items(): score sum(1 for kw in config[keywords] if kw in user_input) score config[priority] if score 0: scores[name] score if not scores: return None return max(scores, keyscores.get) def execute(self, user_input, session_id): agent_name self.route(user_input) if not agent_name: return {error: No matching agent found} agent self.agents[agent_name][instance] context self.state_store.get(session_id, {}) try: result agent.run(user_input, context) self.state_store[session_id] result.get(new_state, context) return result except Exception as e: return {error: str(e), agent: agent_name}3.3 Claude Code与DeepSeek Harness的协同使用这个项目里同时用到了Claude Code和DeepSeek Harness两者的定位不同。Claude Code主要负责代码生成和重构它的优势在于对代码上下文的理解深度DeepSeek Harness主要负责批量任务处理和流程编排它的优势在于成本控制和并发能力。协同使用的关键接口是文件系统。Claude Code生成的代码写到指定目录DeepSeek Harness从目录里读取文件做后续处理。这种“文件系统作为消息队列”的模式看起来简陋但实际用起来非常稳定而且天然支持断点续传——如果某个环节失败了重新跑的时候只需要处理未完成的文件。配置Claude Code的时候有几个注意点。第一是工作目录要设置正确否则它会在错误的位置生成文件。第二是权限控制建议用只读模式挂载不需要修改的目录避免Agent误操作。第三是超时设置代码生成任务有时候会跑很久默认超时时间可能不够需要根据任务复杂度调整。DeepSeek Harness这边安装和配置相对简单但要注意插件加载的顺序。如果插件之间有依赖关系加载顺序错了会导致初始化失败。我遇到过一次harness failed to load plugins的错误排查了半天发现是一个插件依赖的环境变量在另一个插件之后才设置。解决办法是在配置文件里显式声明插件依赖关系让Harness按拓扑排序加载。3.4 Obsidian作为知识库的集成方案Obsidian在这个项目里承担的是“项目记忆”的角色。所有Agent产生的文档、决策记录、代码片段最终都归档到Obsidian的Vault里。这样做的好处是知识可以累积下次遇到类似问题时Agent可以直接检索历史记录不需要重新推理。集成Obsidian的核心是文件格式。Obsidian使用Markdown作为原生格式这和项目的中间表示层天然兼容。但Obsidian有一些自己的扩展语法比如[[双链]]、![[嵌入]]、 [!note]标注块等。在Agent生成内容时需要决定是否使用这些扩展语法。我的建议是内部知识库可以用扩展语法增强可读性但对外输出的内容要用标准Markdown避免兼容性问题。从Zotero导入笔记到Obsidian是一个常见需求。Zotero导出的笔记通常是HTML或者Markdown格式但格式和Obsidian的规范有差异。我写了一个转换脚本核心逻辑是解析Zotero的导出文件提取标题、作者、年份、标签等元数据然后按照Obsidian的模板重新组织。注意Zotero的标签系统和Obsidian的标签系统不完全兼容需要做映射。import re from pathlib import Path def zotero_note_to_obsidian(zotero_md_path, output_dir): 将Zotero导出的Markdown笔记转换为Obsidian格式 content Path(zotero_md_path).read_text(encodingutf-8) # 提取元数据 title_match re.search(r^# (.)$, content, re.MULTILINE) title title_match.group(1) if title_match else Untitled # 提取标签 tags re.findall(r#(\w), content) tag_line .join(f#{t} for t in set(tags)) # 重组内容 body re.sub(r^# .$, , content, count1, flagsre.MULTILINE) obsidian_content f--- title: {title} tags: {tag_line} --- # {title} {body.strip()} output_path Path(output_dir) / f{title}.md output_path.write_text(obsidian_content, encodingutf-8) return str(output_path)4. 大规模Agent项目的工程化管理4.1 20万行代码怎么组织才不会失控一个人维护20万行代码听起来像是天方夜谭但如果这20万行里有80%是Agent生成的事情就变得可行了。关键在于建立一套“生成-审查-归档”的流水线让人类只做决策和审核不做重复劳动。代码组织上我强烈建议按功能域划分目录而不是按技术分层。比如agents/目录下放所有Agent的实现pipelines/目录下放编排流程knowledge/目录下放知识库相关代码。每个目录内部再按具体功能分子目录。这种组织方式的好处是当你需要修改某个功能时所有相关代码都在一个地方不需要在多个层级之间跳转。版本控制方面Git是必须的但提交策略要调整。Agent生成的代码建议单独提交提交信息里标注是哪个Agent生成的、用了什么Prompt。这样出问题的时候可以快速定位是Prompt的问题还是Agent实现的问题。人工修改的代码单独提交和Agent生成的代码区分开。代码审查环节我的做法是设置三道关卡第一道是自动化检查包括语法检查、类型检查、单元测试第二道是Agent互审用一个专门的审查Agent去检查生成代码的质量第三道是人工抽查每天随机抽取10%的生成代码做人工审核。这三道关卡下来代码质量基本可控。4.2 Token消耗的监控与优化40亿token听起来很多但如果不做监控很容易在不知不觉中翻倍。我建议从第一天就建立Token消耗的监控体系按Agent、按任务类型、按时间段三个维度统计消耗。监控数据出来之后优化方向就很清晰了。常见的优化手段包括压缩Prompt长度去掉冗余的示例和说明、缓存重复的查询结果、用更小的模型处理简单任务、批量合并相似请求。这个项目里我印象最深的一个优化是把Agent的System Prompt从2000token压缩到800token整体消耗直接降了15%。还有一个容易被忽视的优化点是输出格式。如果Agent的输出是给另一个Agent消费的用JSON比用自然语言能省不少token。JSON的结构化特性让下游Agent不需要做复杂的解析而且同样的信息量JSON通常比自然语言短。优化手段预计节省比例实施难度适用场景Prompt压缩10-20%低所有场景结果缓存15-30%中重复查询多的场景模型降级20-40%中简单分类、提取任务批量合并10-25%高大量相似请求输出格式优化5-15%低Agent间通信4.3 并发处理与性能调优Agent项目的并发处理和传统Web服务不太一样。传统服务的瓶颈通常在数据库或者网络IOAgent项目的瓶颈在模型API的速率限制和上下文窗口大小。这两个瓶颈决定了你的并发策略。速率限制方面主流模型API都有RPM每分钟请求数和TPM每分钟token数的限制。做并发设计的时候不能简单地开一堆线程去调API那样只会触发限流。正确的做法是实现一个令牌桶或者漏桶算法控制请求的发送速率。同时要做好退避重试遇到429错误的时候指数级增加等待时间。上下文窗口方面每个Agent调用都有窗口大小限制。如果你的任务需要处理很长的文档要么分段处理要么用RAG检索增强生成的方式只取相关片段。这个项目里处理长文档的策略是“分块加摘要”先把文档切成固定大小的块每块生成摘要最后把所有摘要合并成全局摘要。这样既控制了单次调用的token量又保留了文档的整体信息。import time import threading from collections import deque class RateLimiter: def __init__(self, max_requests_per_minute): self.max_rpm max_requests_per_minute self.requests deque() self.lock threading.Lock() def acquire(self): with self.lock: now time.time() # 清理一分钟前的记录 while self.requests and self.requests[0] now - 60: self.requests.popleft() if len(self.requests) self.max_rpm: wait_time 60 - (now - self.requests[0]) if wait_time 0: time.sleep(wait_time) self.requests.append(time.time())4.4 错误恢复与断点续传大规模Agent项目最怕的就是跑到一半挂了然后要从头开始。断点续传机制是必须的核心思路是把每个任务的执行状态持久化到磁盘或者数据库重启的时候从上次中断的地方继续。实现断点续传的关键是任务ID的设计。每个任务要有唯一的ID这个ID要能标识任务的输入、配置和版本。任务执行过程中每完成一个步骤就把状态写到一个checkpoint文件里。重启的时候先读checkpoint跳过已完成的步骤。错误恢复策略要分级别。对于可重试的错误网络超时、限流自动重试对于需要人工干预的错误参数错误、逻辑错误记录到错误队列等人工处理后再重新入队对于不可恢复的错误数据损坏、依赖缺失直接标记任务失败通知相关人员。5. 常见问题与排查技巧实录5.1 Agent输出格式不稳定的处理这是Agent开发中最常见的问题。同一个Prompt有时候输出JSON有时候输出Markdown有时候还夹杂着解释性文字。解决这个问题有三个层次的手段。第一层是在Prompt里明确格式要求并且给出示例。示例要尽可能具体包括字段名、数据类型、嵌套结构。第二层是在Agent输出之后加一个格式校验和修正的步骤用正则表达式或者JSON解析器去提取有效内容。第三层是在编排引擎里做容错如果Agent输出无法解析就触发一次“格式修正”调用让模型重新输出。我的经验是三层都要做但重点放在第一层。一个好的Prompt能减少80%的格式问题。另外用JSON Schema来约束输出格式比用自然语言描述更有效因为模型对结构化约束的遵循度更高。5.2 上下文丢失与状态管理故障多轮对话中上下文丢失是另一个高频问题。表现是Agent在第二轮对话中忘记了第一轮的信息或者把不同会话的状态搞混了。根因通常是状态存储的键设计有问题或者状态更新时没有做并发控制。排查这类问题的第一步是检查session_id的生成逻辑。session_id必须全局唯一且在整个会话期间保持不变。如果session_id是基于时间戳生成的要确保精度足够不会出现碰撞。第二步是检查状态存储的读写是否原子化。多个Agent同时读写同一个session的状态时如果没有加锁会出现覆盖写的问题。我的做法是用Redis做状态存储利用它的原子操作和过期机制。每个session的状态设置一个合理的TTL比如24小时避免状态无限累积。同时用Redis的WATCH/MULTI命令做乐观锁确保状态更新的原子性。5.3 插件加载失败与依赖冲突harness failed to load plugins这个错误在Harness架构的项目里很常见。根因通常是插件之间的依赖关系没有正确声明或者某个插件依赖的环境变量没有设置。排查步骤是这样的先看错误日志确定是哪个插件加载失败然后检查这个插件的依赖列表逐个确认依赖是否满足最后检查环境变量和配置文件确保所有必需的配置项都有值。如果依赖关系复杂建议画一张依赖图用拓扑排序确定加载顺序。预防措施是在项目初期就建立插件的依赖管理规范。每个插件都要有一个manifest文件声明自己的名称、版本、依赖列表、配置项。Harness启动时先读取所有manifest构建依赖图按拓扑排序加载。这样即使插件数量增加也不会出现加载顺序问题。5.4 模型API调用超时与限流API超时和限流是生产环境中最常见的两类错误。超时通常是网络问题或者模型负载高限流则是触发了API的速率限制。对于超时首先要区分是连接超时还是读取超时。连接超时通常是网络问题检查网络配置和DNS解析读取超时是模型处理时间过长可以尝试减小输入长度或者换用更快的模型。重试策略上连接超时可以直接重试读取超时建议先减小输入再重试。对于限流关键是做好请求队列和退避。不要一遇到429就立即重试那样只会加剧限流。正确的做法是实现指数退避第一次等1秒第二次等2秒第三次等4秒以此类推。同时要在客户端做请求合并把多个小请求合并成一个大请求减少请求数量。错误类型典型表现排查方向解决方案连接超时ConnectionTimeout网络、DNS检查网络配置重试读取超时ReadTimeout输入过长、模型负载减小输入换模型限流429 Too Many Requests请求频率过高指数退避请求合并格式错误JSONDecodeErrorPrompt不明确加格式约束后处理上下文丢失状态不一致session_id冲突检查ID生成加锁5.5 代码生成质量波动的应对Agent生成的代码质量不稳定是另一个让人头疼的问题。同一个需求有时候生成的代码可以直接用有时候需要大改。影响代码质量的因素很多Prompt的清晰度、模型的版本、上下文的长度、甚至调用的时间。我的应对策略是建立一套“生成-评估-筛选”的流水线。每个代码生成任务跑三次用不同的温度参数比如0.2、0.5、0.8然后用一个评估Agent给三份代码打分选分数最高的。这样虽然增加了token消耗但代码可用率能提升不少。另外给Agent提供更多的上下文也能提升代码质量。比如在生成某个函数的代码时把相关的类型定义、接口文档、调用示例都放到Prompt里。上下文越丰富模型生成的代码越贴合项目规范。6. 从项目里提炼的可复用经验6.1 一个人做大项目的节奏控制九个月20万行代码平均每天700多行。这个节奏不是靠加班堆出来的而是靠合理的任务拆分和自动化流水线。我的经验是把大目标拆成周目标周目标拆成日目标日目标拆成具体的Agent调用任务。每天早上花半小时规划当天的任务然后让Agent去执行自己只做审核和决策。节奏控制的关键是“留缓冲”。不要把所有时间排满每天留出两小时处理意外情况。Agent项目的不确定性很高今天可能一切顺利明天可能遇到一个诡异的问题卡一整天。有缓冲时间心态就不会崩。6.2 工具链的选型原则这个项目里用到的工具很多Claude Code、DeepSeek Harness、Obsidian、Typora、VS Code等等。选型的原则是“成熟优先、接口开放、社区活跃”。不要为了尝鲜去用刚发布的工具除非你有能力自己修bug。具体来说Markdown编辑器选Typora是因为它的渲染效果最接近最终输出知识库选Obsidian是因为它的文件格式开放不会被锁定代码编辑器选VS Code是因为它的插件生态最丰富。每个工具都要有替代方案万一某个工具出问题了可以快速切换。6.3 知识管理的长期价值项目过程中产生的文档、决策记录、代码片段如果不好好管理项目结束后就变成了一堆垃圾。Obsidian的价值在于它把这些零散的信息组织成了可检索、可关联的知识网络。我的做法是每完成一个模块就写一篇总结笔记记录这个模块的设计思路、遇到的问题、解决方案。笔记之间用双链关联形成知识图谱。项目结束后这些笔记就是最好的技术资产下次做类似项目的时候可以直接复用。6.4 成本控制的实战技巧40亿token听起来很多但通过合理的优化实际成本可以控制在很低的水平。除了前面提到的Prompt压缩、结果缓存、模型降级之外还有一个技巧是“错峰调用”。很多模型API在凌晨时段的费率更低把批量处理任务安排在凌晨跑能省不少钱。另一个技巧是“分级处理”。不是所有任务都需要用最强的模型简单的分类、提取任务用便宜的小模型就够了。只有复杂的推理、生成任务才用大模型。这个项目里大概70%的调用用的是小模型只有30%用大模型整体成本降了一半以上。6.5 后续扩展的方向这个项目的架构设计留了不少扩展空间。比如可以增加更多的Agent来覆盖新的领域可以接入更多的模型来提升效果可以把编排引擎做成分布式的来提升吞吐量。但扩展的前提是核心架构足够稳定不要为了扩展而扩展。我个人觉得最有价值的扩展方向是“Agent的自我进化”。让Agent根据历史执行记录自动调整Prompt和参数逐步提升执行效果。这个方向目前还在探索阶段但已经有一些初步的实践案例效果还不错。
返回列表