很多人接 RAG 时把 harness 当成一个黑盒框架,结果检索能跑、回答也能出,但一上量就超时、一改 prompt 就退化、一换模型就全乱,还找不到是哪一环坏了。这篇不聊协议兼容和客户端配置,只讲 harness 这一层的工程做法:它该负责哪些职责、边界画在哪、超时和并发怎么设、失败怎么降级、日志记什么、上线前用哪几条用例做回归。
很多人接 RAG 时把 harness 当成一个黑盒框架,代码一跑检索能出结果、回答也能生成,看起来挺顺。可一上量就出问题:延迟忽高忽低、改一句 prompt 效果就退化、换个模型整条链路全乱,最要命的是出错之后找不到是哪一环坏的。
这篇不聊协议兼容,也不聊客户端怎么配。只说 harness 这一层的工程做法:职责边界画在哪、流水线做成什么形态、超时和并发怎么设、失败怎么降级、日志记什么、上线前拿哪几条用例做回归。下面按这几件事逐条讲,每节都给判断标准和可执行动作。
harness 到底管什么:先划清它和检索、模型两层的边界
harness 的核心职责是编排,把「改写查询→检索→重排→拼上下文→调模型→校验输出→回填引用」这几步按顺序串起来,接管每次调用之间的状态传递和失败处理。它自己不该做向量计算,也不该做生成,这两件事分别属于检索层和模型层。
- 归 harness 管
- 流程编排、超时控制、重试与降级、上下文拼装、输出校验、引用回填、链路日志
- 不归 harness 管
- 文档切片与入库、向量库索引构建、模型权重与推理性能、向量库本身的查询性能
- 边界判断法
- 一段逻辑换个向量库或换个模型就要重写,它就不该待在 harness 里;换模型只改一处配置,说明边界画对了
这条边界为什么重要?因为检索层和模型层出问题时,如果你把它们的行为混在 harness 里打补丁,后面换任何一个组件你都分不清是补丁失效还是组件本身变了。比如检索召回质量差,正确做法是去查切片和索引,而不是在 harness 里加一段「如果召回少于 N 条就换个查询再试」——那只是把问题藏起来。
流水线形态怎么定:同步串、并行扇出还是带工具循环
形态选择不是看哪个先进,而是看你的瓶颈在哪。同步串行链路最短、日志顺序最清晰,出了错一眼能定位到是哪一步,适合先跑通再优化。并行扇出适合多路召回或多查询改写,收益在延迟,代价是合并逻辑和部分失败的处理要提前想清楚。
| 形态 | 适合场景 | 主要代价 | 要注意什么 |
|---|---|---|---|
| 同步串行 | 链路简单、先跑通、排错优先 | 延迟是各段之和 | 每段超时单独设,别只设总超时 |
| 并行扇出 | 多路召回、多查询改写、检索是瓶颈 | 合并逻辑复杂、部分失败要处理 | 哪一路失败就用哪一路的结果,别等全部完成 |
| 带工具循环 | 模型自己决定要不要再检索 | 难控、可能挂很久 | 必须配步数上限和单步超时 |
- 检索慢就并行检索,把多路召回同时发出去,别串着等
- 生成慢就先做上下文裁剪和缓存,不是改架构
- 工具循环形态下,模型每多决定一次检索就多一次延迟,步数上限是硬约束
- 别用架构改造去掩盖单点慢:先把每一段单独计时,找到最慢那段再决定形态
超时怎么分档:别用一个超时管整条链路
用一个总超时管整条链路,最常见的结果是:某一段慢一点就整条失败,而且你完全不知道慢在哪。正确做法是把总预算拆成检索、重排、生成三段,各段单独设限,某段超时只降级那一段。
- 先量基线到 /tools/speed-test 量一遍自己的检索和生成基线延迟,超时值按基线加余量,不要拍脑袋。
- 拆分超时检索、重排、生成各设一个读取超时;连接超时和读取超时分开设,读取超时按生成长度分档。
- 区分首字和整段流式输出要用心跳或首字超时判断,不能等整段读完才判断超时,否则用户等半天才看到报错。
- 按顺序排查先看是首字慢还是整段慢,再看是检索慢还是生成慢,最后才怀疑网络,别一上来就调大超时值。
这里有个容易搞反的点:调大超时值能让请求「成功」,但用户感知的等待时间一点没少。超时本质上是给你一个明确的失败边界,让你决定超时之后怎么办,而不是把等待拉长。到 /price-watch 核对当时的调用情况和价格变化时,你会发现成本高的往往不是超时本身,而是超时之后无脑重试。
并发与背压:把限流当成设计的一部分
并发不是越大越好。入口不限流、队列不设上限,请求会全部堆在内部,检索和模型两边都被拖垮,结果是本来能正常返回的请求也一起超时。背压要当成设计的一部分,从入口就限并发。
- 队列给上限,满了直接返回排队结果或走降级,不要无限堆积
- 批量任务分批跑、按批间隔,失败单独重投,不要把整批成败绑在一起
- 重试只对可重试的错误做,指数退避加上限次数;鉴权和参数类错误重试没用,直接记日志返回
- 判断标准:压测时如果错误率随并发上升而上升,说明你缺的是背压,不是更大的并发额度
这条判断标准很实用:如果加并发之后吞吐没上去、错误率反而上去了,再加并发只会让情况更糟。这时候该做的是把队列长度和并发数往下压,让系统在可承受范围内稳定跑,再去找真正的瓶颈。
降级怎么做:让失败的情况下还能给出有用的东西
降级的思路是按影响面分层,而不是一刀切报错。重排失败就用原始召回顺序,多路召回失败就退单路,生成失败就返回检索片段加引用。每一层降级损失的只是质量的一小部分,用户还能拿到东西。
| 失败环节 | 降级动作 | 用户拿到什么 | 要标记什么 |
|---|---|---|---|
| 重排 | 按原始召回顺序拼上下文 | 排序略差但内容完整 | 重排未生效 |
| 多路召回 | 只保留主力那一路 | 召回面变窄 | 单路召回 |
| 生成 | 返回检索片段加引用 | 有依据但没成文 | 未经生成 |
| 全链路 | 明确报错并给重试入口 | 报错信息 | 失败原因 |
有一种情况要单独说:合规、医疗、法务这类场景,宁可明确报错,也不要给出未经生成或未经核验的片段。降级策略要按业务风险单独定义,不能照搬通用那套。这里不适用「先有输出再优化」的思路。
日志记什么:让一次慢请求和一次错回答都能复盘
日志的价值在复盘。一次答错,你要能判断是召回根本没找到,还是找到了但排序排错了,还是上下文里塞进去了但模型没用好。要做到这一点,日志必须记到检索结果 id 和重排后的顺序。
- 一次请求记一条主记录:查询原文、改写后的查询、召回的文档 id 列表、重排后的顺序、送进模型的上下文长度、各段耗时、是否命中缓存
- 失败单独记:失败在哪一段、错误类型、是否触发重试、第几次重试、最终走了哪条降级路径
- 日志里别塞完整上下文正文,用文档 id 加版本号引用,正文另存,否则体积失控还容易泄露内容
- 耗时按段拆开记,不要只记一个总耗时,总耗时没法定位
只有记到这些字段,你才能回答「这次为什么慢」和「这次为什么答错」这两个问题。反过来说,如果日志里只有一条总耗时和一段最终回答,那出了问题只能靠猜,猜的成本比记日志高得多。
上线前怎么验:小用例集加固定输入,比调参数有用
每次改 prompt、换模型、调切片,都应该跑同一组用例,看变化在哪。用例不用多,二十条左右就够,但要覆盖四类:能答的、该拒答的、需要引用的、容易混淆的。
| 看什么 | 怎么判断 | 不通过说明什么 |
|---|---|---|
| 召回是否命中 | 正确来源是否在召回列表里 | 切片或索引有问题 |
| 排序是否合理 | 正确来源是否排在前列 | 重排或相似度计算有问题 |
| 回答是否被支撑 | 回答内容能否在上下文里找到依据 | 模型或提示有问题 |
| 引用是否真实 | 引用是否指向召回列表里的来源 | 回填逻辑有问题 |
模型和检索要分开评。混在一起评,只会得出「换了模型变好了」这种没法行动的结论。真要说口径,可以到 /tools/verify 看检测与评分规则;自己定标准时,写清什么算通过、什么算部分通过,避免每次结论不一致。
几个容易踩的边界:什么情况下这套做法不成立
上面讲的都是通用做法,但有几类场景不适合照搬。硬套只会增加故障面,或者把简单问题复杂化。
- 数据量很小、查询模式固定时,硬上多路召回加工具循环只会增加故障面,简单方案更快更稳
- 要求强一致的场景,比如对账、法务条款比对,不能让模型自由改写查询,改写会引入不可控偏差
- 纯离线批处理不关心延迟,超时和并发策略要按吞吐重排,照搬在线那套会浪费预算
- 文档更新频繁时,harness 里要带上知识版本判断,否则检索用旧索引、生成用新提示,结论会自相矛盾
另外,如果你的团队只有几个人、日请求量也不高,先去 /sites 看看同类项目是怎么做的,比自己从零设计一套编排层更省时间。选型和解法参考可以看 /rankings 里按场景筛出来的方向,但记住那是参考,不是标准答案。
下一步拿一条你现在最常出问题的请求,按上面三段拆开计时,看看瓶颈到底在检索、重排还是生成;再去 /tools/speed-test 量一遍基线,把超时和降级按基线重设一轮。→常见问题
DeepSeek harness 是官方提供的东西还是自己写的一层?
它通常指你自己在应用和模型之间写的那层编排代码,不是某个必须引入的框架。要不要单独抽一层,取决于你的链路有没有多段、要不要复用、出问题时需不需要分段定位。单文档小脚本没必要抽。
接了 harness 之后回答还是不对,先查哪一段?
按「召回有没有找到—重排有没有排上来—上下文有没有被裁掉—模型有没有按上下文答」的顺序查,每段看日志里的文档 id 和耗时。跳过前两步直接调 prompt,往往改的是症状不是原因。
超时值设大一点是不是就没问题了?
设大只把失败往后推,用户等待时间照样长,还会占住并发位。正确做法是分段设超时加对应降级,让慢的那一段被替换掉而不是让整条请求拖死。先量基线延迟再定数值。
多路召回和重排是不是必须上?
不一定。先看当前失败是「没召回」还是「排错了」:没召回的话加一路召回有意义,排错了才该上重排。两样都上会让延迟和排错难度一起涨,收益未必抵得上。
怎么判断一次改动到底有没有变好?
用固定的小用例集跑前后对比,分别看命中、排序、是否被上下文支持、引用是否真实四项。只看总分容易得出错误结论,因为有的改动是用拒答换来了更高的表面准确率。