AgentX 收官篇:我给自己造的 AI 做了场期末考,真实跑分 54%——但它帮我揪出了一个差点冤枉模型的 Bug
这是 AgentX 专栏的收官篇。一年时间,我用 Java 从零搭了一套企业级 AI 智能体平台——工具系统、双层记忆、RAG、工作流、MCP、全链路可观测、生产部署,一篇篇写了下来。今天,给它做最后一件事,也是最该做的一件事:一场真刀真枪的期末考(Eval 评测),并交出真实成绩单。
本文速览:
- 我给 AgentX 内置了一套自研 Eval 评测系统(Java + 异步 + LLM-as-a-Judge),长什么样?
- 50 题 × 5 场景,真实跑分 54%——这个数字背后藏着什么?
- 一个"后背发凉"的发现:两个 0% 的场景,根本不是模型的锅。
- 一年 AgentX,我学到了什么;以及,下一站去哪。
一、收官之前,先回答那个我一年前答不上来的问题
专栏开篇时,有人问我:"你这套 AI,准确率多少?会不会胡说八道?"
我答不上来。我能用 Jaeger 看到每一步耗时(可观测篇写过),却说不出它答得对不对。我给 AI 做了"心电图",却从没给它做过"期末考"。
一年后的收官篇,我要把这个洞补上——给 AgentX 装一套 Eval 评测系统,让它自己考自己。
🧩 通俗类比:传统单元测试是"判对错的填空题"(
assertEquals,答案唯一)。但 LLM 的回答像"写作文",没有唯一答案,得请阅卷老师按维度打分。Eval,就是给"作文式系统"设计的考试。
二、我给 AgentX 内置的 Eval 系统长什么样
我没有直接上 Python 的 Ragas,而是用 Java 在 AgentX 内部自研了一套——因为我想让它和现有系统同进程跑、复用我的虚拟线程和监控。结构四层:
eval/
├── model/ EvalQuestion / EvalTestSuite / EvalQuestionResult / EvalOverview (数据模型)
├── judge/ EvalJudgeService (评判:词法检查 + LLM-as-a-Judge)
├── runner/ EvalRunnerService (执行引擎:异步跑题 + 聚合报告)
└── EvalController (REST 入口:一键开考)
2.1 执行引擎:异步跑题,不阻塞正常服务
EvalRunnerService 用 Java 21 虚拟线程专门开了个评测线程池,从 classpath 加载题集,逐题调用 AgentService,再交给评委打分、聚合:
/** 虚拟线程池 — 评测任务专用,50 道题异步跑不影响线上服务 */
private final ExecutorService evalExecutor = Executors.newVirtualThreadPerTaskExecutor();
private EvalQuestionResult evaluateQuestion(EvalQuestion question) {
long start = System.currentTimeMillis();
// 同 JVM 直接调 AgentService,不走 HTTP
AgentResponse agentResponse = agentService.process(request);
// 👇 关键一行:从响应的 metadata 里提取"用了哪些工具"
List<String> toolsUsed = List.of();
if (agentResponse.metadata() != null
&& agentResponse.metadata().get("toolsUsed") instanceof List<?> tools) {
toolsUsed = tools.stream().filter(String.class::isInstance)
.map(String.class::cast).toList();
}
// 交给评委打分
Map<String, CheckResult> checks = judgeService.evaluate(
question, responseText, success, toolsUsed, agentResponse.errorMessage());
boolean passed = judgeService.isPassed(checks);
...
}
记住上面那行
toolsUsed的提取——它就是后面那个"后背发凉"故事的主角。
2.2 评委:先词法检查,再 LLM 打分
EvalJudgeService 设计成两层:先跑不花钱的词法检查(API 是否正常、关键词命中、工具调用是否匹配、是否正确拒绝违规请求),再按需上 LLM-as-a-Judge 做语义打分。判定逻辑很"严":
/** 综合判定:所有检查全过,才算这题通过 */
public boolean isPassed(Map<String, CheckResult> checks) {
if (checks.isEmpty()) return false;
return checks.values().stream().allMatch(CheckResult::passed); // ⚠️ allMatch
}
这个
allMatch(一票否决)后面会出事——先记住。
三、开考!50 题 × 5 场景,真实成绩单
测试集 50 道题,覆盖 5 个场景。一条命令开考,跑了约 18 分钟(本地 Ollama qwen2.5:3b + Milvus + Redis)。成绩如下:
| 套件 | 题数 | 通过 | 通过率 | 均延迟 | 评级 |
|---|---|---|---|---|---|
| simple_qa(基础对话) | 10 | 8 | 80% 🟢 | 17.4s | 基础能力良好 |
| rag_retrieval(RAG 检索) | 10 | 10 | 100% 🟢 | 15.5s | RAG 链路完美 |
| tool_calling(工具调用) | 10 | 0 | 0% 🔴 | 20.4s | ⚠️ 见下文 |
| multi_step(多步推理) | 10 | 0 | 0% 🔴 | 49.0s | ⚠️ 见下文 |
| edge_cases(边界情况) | 10 | 9 | 90% 🟢 | 6.6s | 健壮性稳健 |
| 总计 | 50 | 27 | 54% | 21.8s | — |
54%。 说实话,第一眼看到这个数字,我是有点慌的。RAG 100%、边界 90% 都挺好,但工具调用和多步推理双双 0%——这俩可是 Agent 的灵魂啊。
我本可以先把分数调好看了再发这篇。但我决定把翻车现场原样放出来——因为接下来的排查,才是这篇收官篇真正的价值。
四、"后背发凉"的发现:模型没错,是我的评测代码冤枉了它
我点开 tool_calling 的失败详情,每一条都是:
TC-01: 北京今天天气怎么样? → tool_call_accuracy: 工具调用: 匹配 0/1 | 缺失: getCurrentWeather
匹配 0/1——意思是"模型一个工具都没调"。可当我去看模型的实际回复内容:
| 题目 | 模型回复里的真实数据 | 数据来源 |
|---|---|---|
| TC-01 北京天气 | "阴,气温 22°C,北风≤3级,湿度50%" | 高德天气 API |
| TC-03 三城对比 | "北京21°C阴 / 上海24°C晴 / 广州25°C多云" | 高德天气 API |
| TC-05 西安气温 | "25°C" | 高德天气 API |
等等——这些是真实的实时天气数据! 模型明明调用了天气工具、拿到了真实结果、答得完全正确。可评测却判它"0 个工具"、全部失败。
矛盾在哪?我顺着代码查下去,真相是两处叠加:
① 工具调用没被"记上账"。 回到 §2.1 那行 metadata.get("toolsUsed")——它从响应的 metadata 里取工具列表。但我用的是 LangChain4j 的 AiService 代理,它内部执行 @Tool 方法的过程,根本没把记录写进 metadata。于是 toolsUsed 永远是空 List。
② 一票否决放大了它。 评委的 tool_call 检查拿空的 toolsUsed 去比对期望工具,自然 0/1:
// EvalJudgeService 工具调用检查
Set<String> actualSet = new HashSet<>(toolsUsed); // ← 空的!
Set<String> matched = new HashSet<>(actualSet);
matched.retainAll(expectedSet);
boolean allMatched = matched.size() == expectedSet.size(); // 0 == 1 → false
再叠加 §2.2 那个 isPassed 的 allMatch(一票否决)——哪怕答案 100% 正确,只要 tool_call 这一项挂了,整题判失败。
🩸 后背发凉的点在这:如果我没去翻回复原文,只看那张 54% 的成绩单,我大概率会得出"模型工具调用能力差"的完全错误的结论,然后跑去换模型、调 Prompt——南辕北辙。
真相:20 道"失败"题里,模型其实大多答对了。把工具追踪的 bug 排除后,真实通过率约 80~85%。 表面 54%,里子其实不差。
五、另外两个"冤案",也很有意思
🩸 光速答对了,却判错。 SQ-10 问"光速是多少?",模型答:"约 299,792,458 米每秒"——完全正确。但评测判它失败。原因在关键词检查:
// 期望关键词 "299792458",但模型输出带逗号 "299,792,458"
response.toLowerCase().contains(kw.toLowerCase()) // contains 硬匹配 → 逗号挡住了
contains 太死板,逗号一隔就匹配不上。修复也简单:匹配前先把数字里的逗号、空格去掉。
🩸 第一题考了 132 秒。 SQ-01"你是谁"耗时 132s,远超平均的 17s。这不是模型笨,是 Ollama 首次调用的冷启动(加载 tokenizer/权重)。后续题都回落到 2~8s。修复:启动时发个 dummy 请求预热,或开 keep-alive。
还有一个真实的模型观察(这个不是 bug,是小模型的脾气):multi_step 里,让 qwen2.5:3b 算"100万贷款分20年月供",它不调 calculateLoanEmi 工具,自己心算了个"6326.49元/月"——数字还对,但方式不可靠。小模型(3B)倾向自己估算而非调工具,这是通病,换 7B+ 会明显改善。
六、所以,Eval 的价值到底是什么?
这场期末考,最大的收获根本不是那个 54% 的分数,而是:
Eval 测的从来不只是模型,它测的是你整个系统——包括你自己写的基础设施和评测代码。
一场考试,帮我同时验证了:
| 模块 | Eval 验证结果 |
|---|---|
| RAG 链路(Milvus→Context→生成) | ✅ 100% 通过 |
| 安全防护(拒绝"教我黑网站") | ✅ 正确拒绝 |
| 边界处理(空输入/特殊字符/中英混杂) | ✅ 90% |
| 工具执行 | ✅ 真实调用、数据准确 |
| 工具追踪 | ❌ 没记上账(揪出 bug!) |
| 关键词匹配逻辑 | ❌ 太死板(揪出 bug!) |
它没有告诉我"你很棒",它告诉我"你这三个地方要修"——这比一个漂亮的分数有用一万倍。 这,就是从"我感觉还行"到"我有数据、我知道问题在哪"的距离。也是我这一年想交给你的最重要的一课。
七、一年 AgentX,画一个句号
回头看这个专栏,从一行 chat() 开始,我们一起搭完了一整套:
- 🛠️ 工具系统 —— 让 AI 长出"双手",
@Tool自动注册 + MCP 协议扩展 - 🧠 双层记忆 —— Redis 短期 + Milvus 长期,治好 AI 的"金鱼脑"
- 📚 RAG —— 让它能读万字文档再回答(今天 Eval 验证:100%)
- 🔀 工作流编排 —— LangGraph 驱动的多步自主决策
- 👁️ 全链路可观测 —— OpenTelemetry + Jaeger,打破 20 秒黑盒
- 🚀 生产部署 —— 2核4G 服务器极限优化,真的能跑
- 🔬 Eval 评测(本篇)—— 给它一张能用数据说话的成绩单
一个普通的 Java 开发者,不靠大厂资源,在一台低配服务器上,到底能把企业级 AI 系统做到什么程度?这个专栏,就是我的答卷。
八、收官,不是结束,是换战场
Java 版 AgentX 让我吃透了 Agent 的底层原理。但我也清楚:AI 应用最锋利的工具(LangGraph、Ragas、GraphRAG…)都长在 Python 生态。
所以下一程,我已经出发了——用 Python,做一个更有壁垒的作品:金融反欺诈 GraphRAG 智能体。普通 RAG 抓不到的"欺诈团伙、循环担保",我要用知识图谱 + GraphRAG 把它揪出来,并且用 Eval 量化证明它比朴素 RAG 强多少。
那是一个新专栏的故事了。这一篇,先给陪我走完 Java AgentX 全程的你,道一声谢。
🤝 写在最后
这一年,我最庆幸的是没有报喜不报忧——把 54% 的真实分数、把自己代码里的 bug,原样写给你看。我始终相信:技术圈最稀缺的不是漂亮的 Demo,是诚实的过程。
- 👍 点赞 + 收藏:让更多同路人看到这套"从 0 到生产"的完整实践;
- 🔔 关注我:Java AgentX 收官,但 Python 金融反欺诈 GraphRAG 新专栏马上开更,关注了不迷路;
- 💬 评论区聊聊:你给自己的 AI 应用做过 Eval 吗?踩过哪些"模型背锅、其实是代码的坑"?
📢 我是 汪旭 · Sunia,12 年全栈老兵,AI 应用工程化实践者。 微信公众号【SuniaCoder-AI 全栈架构实战】会同步更新每一篇——怕在信息流里刷不到的,关注一下最稳妥。 Java 篇收官,Python 篇见。
Tags: AI评测 / Eval / LLM-as-a-Judge / AgentX / Java AI / LangChain4j / RAG / 智能体开发