本文基于 ai-customer-service-lab 项目源码分析,结合 2026 年业界对 Harness Engineering 的讨论,探讨如何把「套在模型外面的工程体系」做成可复现、可度量、可演进的一等公民。


一、2026 年的范式转移:从 Prompt 到 Harness

如果你做过两年以上的 LLM 应用,大概会经历这样的演进路径:

  1. Prompt Engineering(2022–2024):写好系统提示词,调 temperature,祈祷模型听话。
  2. Context Engineering(2025):RAG、Memory、Tool Calling 堆上去,上下文越来越长。
  3. Harness Engineering(2026):问题不再是「怎么写一句更好的 prompt」,而是「怎么设计一整套运行环境,让 Agent 持续、可靠、可审计地工作」。

Martin Fowler 的 Harness Engineering 文章 用了一个很贴切的比喻:Agent = 模型 + Harness(挽具/外骨骼)。外骨骼做两件事:

  • 前馈控制(Guides):在执行前引导 Agent 走对路——Prompt 模板、路由规则、工具白名单、权限边界。
  • 反馈控制(Sensors):在执行后观测结果——评估打分、Trace 快照、关键词命中、人工审核。

两者缺一不可。只有前馈,你不知道引导是否真的有效;只有反馈,Agent 会反复犯同样的错。

OpenAI、Anthropic 在 2026 年初密集发文,核心共识是:模型的非确定性不会消失,但 Harness 可以把不确定性关进可控的管道里。Anthropic 甚至发现,让模型自我评估往往过于宽松,分离出独立的 Evaluator Agent 才是成熟 Harness 的标志。

这个实验室项目,恰好把这套思想落到了代码里。

二、项目全景:一个为 Harness 而生的 AI 客服实验室

ai-customer-service-lab 不是一个「能聊天的 Demo」,而是一个 能力演进 + 评估对比 的实验场:

层级技术职责
后端Java 17 + Spring Boot 3.3,14 个 Maven 模块模块化编排、评估 Harness、双端口部署
前端React 19 + Vite + Zustand聊天调试、五档能力对比、Agent Trace 可视化
LLMLangChain4j(OpenAI 兼容 API)生产路径;评估路径用 Stub 替代

模块依赖自底向上清晰分层:

ai-common (SPI 契约)
  → ai-core / ai-prompt / ai-rag / ai-memory / ai-tools / ai-agent-router
    → ai-service (唯一编排入口)
      → ai-eval (评估 Harness)
        → ai-reactive-chat (8081) | ai-admin-webmvc (8080)

关键设计决策:所有路径都走同一个 AiChatService。生产环境调真实 LLM,评估环境换 Stub LLM——管道不变,变量可控。这正是 Harness-Bench 等学术基准所强调的「隔离 Harness 效应」思路。

三、编排管道:Harness 的「办公室」

AiChatService 是整个系统的唯一编排入口,注释写得很直白:

/**
 * 系统唯一编排入口:Agent 路由决策 → 聚合 memory / RAG / tools,经 prompt 构建后调用 LLM,不写具体领域实现。
 * <p>
 * 流程:load memory → {@link AgentRouter} →(可选)RAG →(可选)tools → build prompt → LLM → save memory。<br>
 * 路由可由 LLM(JSON)或规则实现,见 {@code aics.orchestration.agent-router-llm-enabled}。
 */

每一轮对话的执行链路:

loadHistory(sessionId)
  → agentRouter.route(message, history)        // 决策:要不要 RAG?调哪个 Tool?
  → [optional] rag.retrieve(message)           // 双门控:全局开关 AND 路由决策
  → [optional] tools.executeNamed(toolName, message)
  → promptComposer.build(history, context, toolResult, message)
  → llm.chat(prompt)
  → memory.saveMessage(sessionId, message, answer)
  → ChatTurnTraceResult(含完整快照)

这里有一个容易被忽略但非常重要的 双门控(Product Switch × Agent Decision) 模式:

        boolean useRag = orchestrationProperties.isRagEnabled() && decision.useRag();
        List<String> context = useRag ? rag.retrieve(message) : Collections.emptyList();

        boolean useTools = orchestrationProperties.isToolsEnabled() && decision.useTools();
        String toolResult = useTools
                ? tools.executeNamed(decision.toolName(), message)
                : "";

全局开关和 Agent 决策做 AND 组合——运维可以一键降级 RAG,而 Agent Router 仍然可以精细决策。这是典型的 前馈控制分层:产品级开关在上,语义级路由在下。

chatWithTrace() 返回的 ChatTurnTraceResult 不是日志旁路,而是 API 响应的一等字段。开发模式下,前端可以直接展开「Agent 编排调试」面板,看到决策 → RAG 片段 → 工具结果 → 完整 Prompt 的完整链路。这就是 在线 Sensor

四、Agent Router:Harness 里的「调度员」

在 FULL 能力档位下,系统多了一层 元决策:不是直接回答问题,而是先决定「这轮需要哪些能力」。

Router 的 Prompt 强制 JSON 输出,键名固定、工具白名单枚举:

    private static final String TEMPLATE = """
            你是一个 AI 客服系统的「能力路由决策器」。根据用户当前问题(必要时结合历史摘要),判断本轮是否需要:
            1)检索知识库(RAG)
            2)调用业务工具(Tools)

            可用工具名称(toolName)仅限以下之一;若不需要工具则 useTools 为 false,toolName 为空字符串 "":
            - echo:用户明确要求回显或调试,或消息以 echo: 开头
            - order_query:订单、物流、发货、ORD- 单号等相关
            - weather_query:天气、气温、降雨等
            ...
            只输出一个 JSON 对象,不要 Markdown 代码块,不要其它解释文字。键名必须完全一致:
            {"useRag":true或false,"useTools":true或false,"toolName":"字符串或空","reason":"一句话中文理由"}

生产环境有三种 Router 实现,形成 Graceful Degradation 链:

实现机制场景
LlmAgentRouter额外一次 LLM 调用,解析 JSON 决策默认开启
RuleBasedAgentRouter启发式关键词匹配LLM 关闭或降级
FallbackAgentRouterLLM 失败时自动回退规则路由生产容错

Router 本身也是 Harness 的一部分——它限制了模型能「看到」哪些工具、能「触发」哪些能力。没有白名单,Agent 可能幻觉出一个不存在的 refund_tool

五、五档能力模型:可度量的演进阶梯

项目用 AiVersion 枚举定义了五个能力档位,用于横向对比:

public enum AiVersion {
    BASE,
    PROMPT,
    RAG,
    MEMORY,
    FULL
}
档位启用能力模拟场景
BASE裸 LLM,用户问题直接透传「仓库可能比较忙吧,我也不确定」
PROMPT+ 客服角色与段落结构有规范,但无业务数据
RAG+ 知识库检索能引用发货时效政策
MEMORY+ 预置会话历史记得用户已付款
FULL+ RAG + Tools订单 123 拣货中,华东仓已揽收

这不是随意分的层——每一层解决一个明确的失败模式:BASE 会幻觉,PROMPT 会泛化,RAG 缺实时数据,MEMORY 缺工具精度,FULL 才是生产目标态。

六、评估 Harness 的核心:CapabilityChatFactory + RecordingLlmClient

这是整篇文章最值得细读的部分。ai-eval 模块构成了一套完整的 离线评估 Harness,代码里没有出现 “harness” 这个词,但模式非常标准。

6.1 确定性装配:CapabilityChatFactory

    public static AiChatService build(AiVersion version, RecordingLlmClient llm) {
        return switch (version) {
            case BASE -> new AiChatService(
                    new EvalChatMemory(""),
                    q -> List.of(),
                    m -> "",
                    new PassthroughPromptComposer(),
                    llm,
                    new FixedAgentRouter(AgentDecision.none()),
                    EVAL_ORCH
            );
            // ... PROMPT, RAG, MEMORY, FULL 各档装配
        };
    }

三个 Harness 设计要点:

  1. 无 Spring 手动装配:与生产代码同路径,但脱离容器依赖,可在 CLI / 单测 / CI 中直接运行。
  2. FixedAgentRouter 消除路由随机性:评估时固定 AgentDecision,隔离「路由不确定性」,专注对比 Prompt/RAG/Memory/Tool 的贡献。这是 Harness Engineering 的关键模式——控制变量,只测你想测的
  3. 内置 Golden DatasetKB 常量(发货时效说明)和 TOOL_JSON 常量(订单 123 状态)即最小 golden set,保证每次评估输入一致。

6.2 Prompt-as-Spec 假 LLM:RecordingLlmClient

    public static String fakeAnswer(String prompt) {
        boolean sys = prompt.contains("你是专业 AI 客服助手");
        boolean rag = prompt.contains("### 参考知识");
        boolean tool = prompt.contains("### 工具结果");
        boolean mem = prompt.contains("### 历史对话");
        if (sys && rag && tool && mem) {
            return "【FULL】订单123当前状态已同步:发货环节进行中;...";
        }
        if (sys && rag && mem && !tool) {
            return "【MEMORY+RAG】根据历史与知识:...";
        }
        // ...
        return "【BASE】大概还没发吧,我也不确定订单和物流细节。";
    }

这个 Stub 的精妙之处在于:它不模拟模型的「智能」,而是模拟「Prompt 结构是否正确传递到了模型」。检测 Prompt 中是否包含 ### 参考知识### 工具结果 等段落标记,返回对应档位的预设答案。

收益显而易见:

  • 零 API 成本:不调用真实模型,CI 秒级完成。
  • 完全可重复:同样输入,永远同样输出。
  • 可断言:进化测试直接 assertThat(llm.lastPrompt).contains("### 参考知识")

这正是 VeRO(Versioning, Rewards, and Observations)论文所倡导的:对 Agent 这种「确定性代码 + 随机 LLM」混合系统,需要结构化捕获中间推理和执行结果。

6.3 评估闭环:EvaluationRunner

    public EvaluationReport run(EvaluationCase evaluationCase) {
        Map<AiVersion, VersionEvaluation> map = new LinkedHashMap<>();
        for (AiVersion v : AiVersion.values()) {
            RecordingLlmClient llm = new RecordingLlmClient();
            AiChatService svc = CapabilityChatFactory.build(v, llm);
            String answer = svc.chat("eval-session-" + v.name(), evaluationCase.question());
            int score = AiEvaluator.score(answer, evaluationCase);
            String explanation = AiEvaluator.explain(answer, evaluationCase);
            map.put(v, new VersionEvaluation(v, answer, score, explanation));
        }
        return new EvaluationReport(evaluationCase, map);
    }

对每个 EvaluationCase(问题 + 期望关键词 + 分类),在全部五个版本上跑一遍,汇总评分,并计算相对 BASE 的分差 deltaFromBase()EvaluationReport.toMarkdownTable() 可以直接生成对比表——写博客时复制粘贴即可。

评分策略有意保持简单:关键词命中率,而非 LLM-as-Judge。教学场景下可解释、零依赖、可手工验证;注释里预留了扩展空间。

七、进化测试:Harness 即活文档

AiServiceEvolutionTest 四个 @Order 有序的测试,用同一个问题「我的订单 123 为什么还没有发货?」,逐步叠加能力:

    @Test @Order(1) void testBaseLLM() { ... assertThat(answer).contains("纯模型"); }
    @Test @Order(2) void testPromptEnhanced() { ... assertThat(llm.lastPrompt).contains("你是专业 AI 客服助手"); }
    @Test @Order(3) void testRagEnhanced() { ... assertThat(llm.lastPrompt).contains("### 参考知识"); }
    @Test @Order(4) void testFullAiService() { ... assertThat(llm.lastPrompt).contains("拣货中"); }

注释里写了一句大实话:

根据 prompt 中是否包含模板块,返回可读的「模拟模型答复」,便于对比阶段差异

这不是事后补的测试,而是 为教学和演示而设计的 Harness 剧本。跑一遍 mvn test -Dtest=AiServiceEvolutionTest,控制台会打印每个阶段的模拟答复,肉眼可见能力叠加的效果。

八、双 Harness 架构:离线评估 + 在线 Trace

这个项目同时运行两套互补的 Harness:

┌─────────────────────────────────────────────────────────┐
│                    离线 Harness (ai-eval)                │
│  CapabilityChatFactory → RecordingLlmClient → 关键词评分 │
│  入口:CLI / POST /api/eval/run / 前端 Eval 页          │
└─────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────┐
│                    在线 Harness (运行时)                   │
│  AiChatService.chatWithTrace() → ChatTurnTraceResult    │
│  前端 AgentDecisionPanel / RagContextPanel / PromptPanel │
│  入口:POST /api/chat(开发模式 expose-prompt-trace)     │
└─────────────────────────────────────────────────────────┘
维度离线 Harness在线 Harness
LLMStub(零成本)真实模型
目的能力档位对比、回归测试单轮调试、排障
评分关键词命中率人工 + Trace 审查
可 CI 化需集成测试环境

Martin Fowler 所说的「如果 Sensor 从不触发,是质量高还是检测不足?」——这个项目给出了一个务实的起点:离线用确定性 Stub 保证管道结构正确,在线用 Trace 暴露真实模型的行为。两者结合,才能建立对 Agent 系统的信任。

九、前馈 vs 反馈:一张对照表

把项目里的组件映射到 Harness Engineering 的双轴框架:

类型组件作用
前馈 · 计算型DefaultPromptComposer、段落标记规范结构化 Prompt,约束模型输入格式
前馈 · 计算型AgentRouterPrompts、工具白名单限制 Agent 可触达的能力边界
前馈 · 计算型OrchestrationProperties 全局开关产品级降级与 A/B
前馈 · 推断型LlmAgentRouterLLM 做语义级路由决策
前馈 · 推断型RuleBasedAgentRouter关键词启发式兜底
反馈 · 计算型AiEvaluator 关键词评分确定性、可解释的 pass/fail
反馈 · 计算型AiServiceEvolutionTest 结构断言Prompt 是否包含预期段落
反馈 · 计算型deltaFromBase()量化每层能力的边际收益
反馈 · 推断型ChatTurnTraceResult + 前端调试面板人工审查完整决策链
反馈 · 推断型PromptEvaluationService 环形缓冲Prompt 构建审计日志

前馈让 Agent「第一次就做对」的概率更高;反馈让你知道「做对了没有」以及「哪一层在贡献」。

十、架构图:端到端 Harness 全景

反馈控制 Sensors

编排核心

前馈控制 Guides

PromptComposer
段落结构规范

AgentRouter
能力路由决策

OrchestrationProperties
全局开关

Tool白名单
echo/order/weather

AiChatService
唯一管道

EvaluationRunner
五档横向对比

RecordingLlmClient
Prompt结构检测

ChatTurnTraceResult
运行时快照

AiServiceEvolutionTest
进化剧本

用户

十一、还能学什么?Harness 的下一步

这个项目已经搭好了 Harness 的骨架,但距离生产级 Harness 还有清晰的演进方向——也正是博客读者可以动手实验的地方:

  1. Evaluator 升级:从关键词命中到 LLM-as-Judge(需独立 Evaluator Agent,避免 Generator 自我放水),或引入 Playwright 端到端验证。
  2. Golden Dataset 扩展DemoData.ORDER_SHIPPING 只是单 case,可扩展为分类数据集(订单、退款、寒暄、越权),形成回归测试集。
  3. Harness 覆盖率:借鉴代码覆盖率思路——哪些意图类别有 golden case?哪些 Tool 路径从未被评估触及?
  4. 模型无关性RecordingLlmClient 已证明管道与模型解耦;下一步是在同一 Harness 上切换 GPT / Claude / 本地模型,量化 Harness 效应(Harness-Bench 的核心问题)。
  5. Sensor 告警PromptEvaluationService 的内存环形缓冲可对接 OLAP,当 Prompt 结构异常(如 RAG 段落缺失)时自动告警。

十二、结语:Agent 的时代,工程师造的是 Harness

回到开头的问题。2026 年做 AI 应用,核心竞争力不再是「谁能写出最炫的 system prompt」,而是:

  • 能否把 编排管道 做成单一、可测试、可 Trace 的入口?
  • 能否用 确定性 Harness 隔离变量,量化每一层能力的边际收益?
  • 能否同时运行 离线评估在线观测,建立对非确定性系统的工程信任?

ai-customer-service-lab 用不到 14 个 Maven 模块、一个 RecordingLlmClient、五档 AiVersion,给出了一份可运行的答案。代码里没有「harness」这个词,但 CapabilityChatFactoryFixedAgentRouterAiServiceEvolutionTestChatTurnTraceResult 合在一起,就是 Harness Engineering 的最佳实践注解。

模型会升级,Prompt 会过时,但 Harness 是留得住的工程资产。

如果你想亲手复现,可以:

  1. 跑进化测试:mvn test -Dtest=AiServiceEvolutionTest(需关闭 skipTests
  2. 跑评估 CLI:mvn exec:java -pl ai-eval
  3. 启动双端口后打开前端 /eval 页,五列卡片直观对比 BASE → FULL
Logo

这里是“一人公司”的成长家园。我们提供从产品曝光、技术变现到法律财税的全栈内容,并连接云服务、办公空间等稀缺资源,助你专注创造,无忧运营。

更多推荐