本文基于 ai-customer-service-lab 项目源码分析,结合 2026 年业界对 Harness Engineering 的讨论,探讨如何把「套在模型外面的工程体系」做成可复现、可度量、可演进的一等公民。
一、2026 年的范式转移:从 Prompt 到 Harness
如果你做过两年以上的 LLM 应用,大概会经历这样的演进路径:
- Prompt Engineering(2022--2024):写好系统提示词,调 temperature,祈祷模型听话。
- Context Engineering(2025):RAG、Memory、Tool Calling 堆上去,上下文越来越长。
- 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 可视化 |
| LLM | LangChain4j(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 是整个系统的唯一编排入口,注释写得很直白:
18:23:ai-customer-service/ai-service/src/main/java/com/aics/service/chat/AiChatService.java
/**
* 系统唯一编排入口: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) 模式:
70:76:ai-customer-service/ai-service/src/main/java/com/aics/service/chat/AiChatService.java
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 输出,键名固定、工具白名单枚举:
8:24:ai-customer-service/ai-agent-router/src/main/java/com/aics/agentrouter/prompt/AgentRouterPrompts.java
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 关闭或降级 |
FallbackAgentRouter |
LLM 失败时自动回退规则路由 | 生产容错 |
Router 本身也是 Harness 的一部分------它限制了模型能「看到」哪些工具、能「触发」哪些能力。没有白名单,Agent 可能幻觉出一个不存在的 refund_tool。
五、五档能力模型:可度量的演进阶梯
项目用 AiVersion 枚举定义了五个能力档位,用于横向对比:
6:12:ai-customer-service/ai-eval/src/main/java/com/aics/eval/AiVersion.java
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
37:84:ai-customer-service/ai-eval/src/main/java/com/aics/eval/support/CapabilityChatFactory.java
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 设计要点:
- 无 Spring 手动装配:与生产代码同路径,但脱离容器依赖,可在 CLI / 单测 / CI 中直接运行。
FixedAgentRouter消除路由随机性 :评估时固定AgentDecision,隔离「路由不确定性」,专注对比 Prompt/RAG/Memory/Tool 的贡献。这是 Harness Engineering 的关键模式------控制变量,只测你想测的。- 内置 Golden Dataset :
KB常量(发货时效说明)和TOOL_JSON常量(订单 123 状态)即最小 golden set,保证每次评估输入一致。
6.2 Prompt-as-Spec 假 LLM:RecordingLlmClient
15:33:ai-customer-service/ai-eval/src/main/java/com/aics/eval/support/RecordingLlmClient.java
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
15:26:ai-customer-service/ai-eval/src/main/java/com/aics/eval/EvaluationRunner.java
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 为什么还没有发货?」,逐步叠加能力:
48:133:ai-customer-service/ai-service/src/test/java/com/aics/service/evolution/AiServiceEvolutionTest.java
@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 |
|---|---|---|
| LLM | Stub(零成本) | 真实模型 |
| 目的 | 能力档位对比、回归测试 | 单轮调试、排障 |
| 评分 | 关键词命中率 | 人工 + Trace 审查 |
| 可 CI 化 | 是 | 需集成测试环境 |
Martin Fowler 所说的「如果 Sensor 从不触发,是质量高还是检测不足?」------这个项目给出了一个务实的起点:离线用确定性 Stub 保证管道结构正确,在线用 Trace 暴露真实模型的行为。两者结合,才能建立对 Agent 系统的信任。
九、前馈 vs 反馈:一张对照表
把项目里的组件映射到 Harness Engineering 的双轴框架:
| 类型 | 组件 | 作用 |
|---|---|---|
| 前馈 · 计算型 | DefaultPromptComposer、段落标记规范 |
结构化 Prompt,约束模型输入格式 |
| 前馈 · 计算型 | AgentRouterPrompts、工具白名单 |
限制 Agent 可触达的能力边界 |
| 前馈 · 计算型 | OrchestrationProperties 全局开关 |
产品级降级与 A/B |
| 前馈 · 推断型 | LlmAgentRouter |
LLM 做语义级路由决策 |
| 前馈 · 推断型 | RuleBasedAgentRouter |
关键词启发式兜底 |
| 反馈 · 计算型 | AiEvaluator 关键词评分 |
确定性、可解释的 pass/fail |
| 反馈 · 计算型 | AiServiceEvolutionTest 结构断言 |
Prompt 是否包含预期段落 |
| 反馈 · 计算型 | deltaFromBase() |
量化每层能力的边际收益 |
| 反馈 · 推断型 | ChatTurnTraceResult + 前端调试面板 |
人工审查完整决策链 |
| 反馈 · 推断型 | PromptEvaluationService 环形缓冲 |
Prompt 构建审计日志 |
前馈让 Agent「第一次就做对」的概率更高;反馈让你知道「做对了没有」以及「哪一层在贡献」。
十、架构图:端到端 Harness 全景
#mermaid-svg-QAYOwDxu67YIHew1{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-QAYOwDxu67YIHew1 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-QAYOwDxu67YIHew1 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-QAYOwDxu67YIHew1 .error-icon{fill:#552222;}#mermaid-svg-QAYOwDxu67YIHew1 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-QAYOwDxu67YIHew1 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-QAYOwDxu67YIHew1 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-QAYOwDxu67YIHew1 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-QAYOwDxu67YIHew1 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-QAYOwDxu67YIHew1 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-QAYOwDxu67YIHew1 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-QAYOwDxu67YIHew1 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-QAYOwDxu67YIHew1 .marker.cross{stroke:#333333;}#mermaid-svg-QAYOwDxu67YIHew1 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-QAYOwDxu67YIHew1 p{margin:0;}#mermaid-svg-QAYOwDxu67YIHew1 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-QAYOwDxu67YIHew1 .cluster-label text{fill:#333;}#mermaid-svg-QAYOwDxu67YIHew1 .cluster-label span{color:#333;}#mermaid-svg-QAYOwDxu67YIHew1 .cluster-label span p{background-color:transparent;}#mermaid-svg-QAYOwDxu67YIHew1 .label text,#mermaid-svg-QAYOwDxu67YIHew1 span{fill:#333;color:#333;}#mermaid-svg-QAYOwDxu67YIHew1 .node rect,#mermaid-svg-QAYOwDxu67YIHew1 .node circle,#mermaid-svg-QAYOwDxu67YIHew1 .node ellipse,#mermaid-svg-QAYOwDxu67YIHew1 .node polygon,#mermaid-svg-QAYOwDxu67YIHew1 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-QAYOwDxu67YIHew1 .rough-node .label text,#mermaid-svg-QAYOwDxu67YIHew1 .node .label text,#mermaid-svg-QAYOwDxu67YIHew1 .image-shape .label,#mermaid-svg-QAYOwDxu67YIHew1 .icon-shape .label{text-anchor:middle;}#mermaid-svg-QAYOwDxu67YIHew1 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-QAYOwDxu67YIHew1 .rough-node .label,#mermaid-svg-QAYOwDxu67YIHew1 .node .label,#mermaid-svg-QAYOwDxu67YIHew1 .image-shape .label,#mermaid-svg-QAYOwDxu67YIHew1 .icon-shape .label{text-align:center;}#mermaid-svg-QAYOwDxu67YIHew1 .node.clickable{cursor:pointer;}#mermaid-svg-QAYOwDxu67YIHew1 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-QAYOwDxu67YIHew1 .arrowheadPath{fill:#333333;}#mermaid-svg-QAYOwDxu67YIHew1 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-QAYOwDxu67YIHew1 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-QAYOwDxu67YIHew1 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-QAYOwDxu67YIHew1 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-QAYOwDxu67YIHew1 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-QAYOwDxu67YIHew1 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-QAYOwDxu67YIHew1 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-QAYOwDxu67YIHew1 .cluster text{fill:#333;}#mermaid-svg-QAYOwDxu67YIHew1 .cluster span{color:#333;}#mermaid-svg-QAYOwDxu67YIHew1 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-QAYOwDxu67YIHew1 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-QAYOwDxu67YIHew1 rect.text{fill:none;stroke-width:0;}#mermaid-svg-QAYOwDxu67YIHew1 .icon-shape,#mermaid-svg-QAYOwDxu67YIHew1 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-QAYOwDxu67YIHew1 .icon-shape p,#mermaid-svg-QAYOwDxu67YIHew1 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-QAYOwDxu67YIHew1 .icon-shape .label rect,#mermaid-svg-QAYOwDxu67YIHew1 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-QAYOwDxu67YIHew1 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-QAYOwDxu67YIHew1 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-QAYOwDxu67YIHew1 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 反馈控制 Sensors
编排核心
前馈控制 Guides
PromptComposer
段落结构规范
AgentRouter
能力路由决策
OrchestrationProperties
全局开关
Tool白名单
echo/order/weather
AiChatService
唯一管道
EvaluationRunner
五档横向对比
RecordingLlmClient
Prompt结构检测
ChatTurnTraceResult
运行时快照
AiServiceEvolutionTest
进化剧本
用户
十一、还能学什么?Harness 的下一步
这个项目已经搭好了 Harness 的骨架,但距离生产级 Harness 还有清晰的演进方向------也正是博客读者可以动手实验的地方:
- Evaluator 升级:从关键词命中到 LLM-as-Judge(需独立 Evaluator Agent,避免 Generator 自我放水),或引入 Playwright 端到端验证。
- Golden Dataset 扩展 :
DemoData.ORDER_SHIPPING只是单 case,可扩展为分类数据集(订单、退款、寒暄、越权),形成回归测试集。 - Harness 覆盖率:借鉴代码覆盖率思路------哪些意图类别有 golden case?哪些 Tool 路径从未被评估触及?
- 模型无关性 :
RecordingLlmClient已证明管道与模型解耦;下一步是在同一 Harness 上切换 GPT / Claude / 本地模型,量化 Harness 效应(Harness-Bench 的核心问题)。 - Sensor 告警 :
PromptEvaluationService的内存环形缓冲可对接 OLAP,当 Prompt 结构异常(如 RAG 段落缺失)时自动告警。
十二、结语:Agent 的时代,工程师造的是 Harness
回到开头的问题。2026 年做 AI 应用,核心竞争力不再是「谁能写出最炫的 system prompt」,而是:
- 能否把 编排管道 做成单一、可测试、可 Trace 的入口?
- 能否用 确定性 Harness 隔离变量,量化每一层能力的边际收益?
- 能否同时运行 离线评估 和 在线观测,建立对非确定性系统的工程信任?
ai-customer-service-lab 用不到 14 个 Maven 模块、一个 RecordingLlmClient、五档 AiVersion,给出了一份可运行的答案。代码里没有「harness」这个词,但 CapabilityChatFactory、FixedAgentRouter、AiServiceEvolutionTest 和 ChatTurnTraceResult 合在一起,就是 Harness Engineering 的最佳实践注解。
模型会升级,Prompt 会过时,但 Harness 是留得住的工程资产。
如果你想亲手复现,可以:
- 跑进化测试:
mvn test -Dtest=AiServiceEvolutionTest(需关闭skipTests) - 跑评估 CLI:
mvn exec:java -pl ai-eval - 启动双端口后打开前端
/eval页,五列卡片直观对比 BASE → FULL