元数据卡
- 前置知识:第17章(事件驱动架构);了解基本的 LLM 概念
- 预计时间:45 分钟
- 核心难度:深入
- 完成标志:能设计 RAG 系统和基本的 Agent 模式
你的进度
你在工匠之都学完了所有经典工艺:从锻造检验到分层组装,从设计模式到引擎拆分。
但 2026 年的工匠之都迎来了一门新的手艺:智能锻炉——它不只是按图纸锻造,它能在锻造中自己学习、自己调整参数。
你的引擎要整合新的智能功能:自动匹配对手实力、用自然语言回答玩家问题。传统的机械逻辑写这些?不可能。你需要把 AI 锻炉接入工匠之都的生产线。 你的任务
AI 系统与传统软件工程最大的区别在于:传统系统是确定性的(同样的输入 → 同样的输出),AI 系统是概率性的(同样的输入 → 有多样输出的可能)。这意味着测试方式、错误处理、部署策略完全不同。这章讲的是 AI 系统的软件工程模式——不是训练模型,是用模型构建系统。
本章分层
- 必读:RAG 架构、评估模式
- 选读:Agent 模式(ReAct)
- 进阶:Cache-Augmented Generation
本章不会要求你掌握
- 模型训练与微调
- Prompt Engineering 技巧
破局 · 溯源
你对接了一个 LLM API:玩家在比赛页面问"这个对手以前赢过我不",LLM 给出回答。效果不错。但你很快发现:LLM 不知道比赛的具体数据——因为它只训练到两年前的数据,不知道昨天的比赛结果。它说"我建议你查一下比赛记录"——这是 AI 的答非所问。
你能做什么?把玩家数据全部塞进 LLM 的上下文?5000 个玩家的数据,token 耗尽,每个请求费用超过 1 美元。
第一层:RAG——检索增强生成
RAG(Retrieval-Augmented Generation)的架构:不把数据塞进模型,把数据塞进数据库。收到用户问题时,先去数据库检索相关文档/数据,再把检索结果和问题一起发给 LLM。
用户提问 → 检索器 → 向量数据库
↓ ↑
└── 检索结果 ────┘
↓
组合成 Prompt → LLM → 回答检索器通常使用向量数据库(如 Pinecone、Chroma、Milvus):把文档转化为向量(embedding),存储起来。用户提问也被转为向量,找到最相似的文档片段。
// ch18/rag/RAGService.java
@Service
public class RAGService {
private final EmbeddingClient embeddingClient; // text → vector
private final VectorStore vectorStore; // 向量数据库
private final ChatClient chatClient; // LLM
public String answerQuestion(String playerId, String question) {
// 1. 把问题转为向量
float[] queryVector = embeddingClient.embed(question);
// 2. 检索相关文档(比赛记录、规则、FAQ)
List<Document> relevantDocs = vectorStore
.similaritySearch(queryVector, 5); // 找 5 个最相关的
// 3. 组合上下文
String context = relevantDocs.stream()
.map(Document::getContent)
.collect(Collectors.joining("\n---\n"));
// 4. 构建 prompt
String prompt = """
使用下面的上下文回答问题。如果上下文不足以回答,
请说"我没有足够的信息来回答这个问题"。
上下文:
%s
问题:%s
回答:
""".formatted(context, question);
// 5. 调用 LLM
return chatClient.generate(prompt);
}
}RAG 给了 LLM 一个"外挂数据库"——不需要把所有信息写死在 prompt 里。每次提问时只检索最相关的一小部分,大大降低了 token 消耗和成本。
第二层:Agent 模式——让 LLM 能行动
你的 RAG 系统只能回答问题。但玩家的问题是"帮我报名明天的擂台赛"——LLM 回答不了"报名"这个操作,因为它没有权限调用你的 API。
Agent 模式让 LLM 不只是"回答问题",而是能够调用工具(函数/API)。ReAct(Reasoning + Acting)是 Agent 的经典实现框架:
用户: "帮我报名明天的擂台赛"
Agent 的思考过程:
1. Thought: 用户想报名。我需要先查询明天的比赛列表,然后执行报名操作。
2. Action: call_function("searchTournaments", {"date": "tomorrow"})
3. Observation: 找到一场 "明日锦标赛",ID: t-123
4. Thought: 找到了比赛。现在需要查询用户信息。
5. Action: call_function("getPlayerInfo", {"playerName": "steven"})
6. Observation: 用户 ID: p-456, level: 25
7. Thought: 用户满足条件。执行报名。
8. Action: call_function("registerForTournament", {"tournamentId": "t-123", "playerId": "p-456"})
9. Observation: 报名成功
10. Final Answer: "已为你报名明日锦标赛!"// ch18/agent/ToolCallingAgent.java
// Agent 的简化实现
public class ToolCallingAgent {
private final ChatClient llm;
private final Map<String, Tool> tools;
public ToolCallingAgent() {
this.tools = Map.of(
"searchTournaments", new SearchTournamentsTool(),
"getPlayerInfo", new GetPlayerInfoTool(),
"registerForTournament", new RegisterTool()
);
}
public String execute(String userInput) {
// 构建带工具描述的 prompt
String toolDescriptions = tools.entrySet().stream()
.map(e -> e.getKey() + ": " + e.getValue().getDescription())
.collect(Collectors.joining("\n"));
String systemPrompt = """
你是一个帮助玩家管理比赛的助手。
你有以下工具可用:
%s
每次你决定调用工具时,回复格式:
ACTION: 工具名
ARGS: JSON 格式的参数
当你得到足够的信息来回答用户时,回复:
FINAL: 你的回答
""".formatted(toolDescriptions);
String response = llm.generate(systemPrompt, userInput);
// 如果 LLM 选择调用工具
if (response.startsWith("ACTION:")) {
String toolName = extractAction(response);
String args = extractArgs(response);
Tool tool = tools.get(toolName);
String result = tool.execute(args);
// 把结果给 LLM,让它继续推理
return execute("工具返回: " + result);
}
// 如果 LLM 直接回答
return extractFinal(response);
}
}Agent 的关键挑战:LLM 可能持续循环(永远输入工具调用 -> 工具返回 -> 再调用工具)。必须设最大迭代次数:
public String execute(String input, int maxSteps) {
for (int i = 0; i < maxSteps; i++) {
// ... Agent 循环 ...
if (isFinal(response)) return extractFinal(response);
}
return "抱歉,处理超时了。请重新提问。";
}第三层:Cache-Augmented Generation——缓存与 LLM 的结合
RAG 解决了"知识更新"的问题,但每次提问都去查数据库再调 LLM——一个典型请求的端到端延迟是 2-5 秒。对于"什么是比赛规则?"这种有一千个人问过的问题,完全不需要每次都走 LLM。
Cache-Augmented Generation 的核心思想是将 LLM 的输出缓存起来——对于相同或高度相似的问题,直接返回缓存结果。
// ch18/cache/CachedRAGService.java
@Component
public class CachedRAGService {
private final RAGService ragService;
private final Cache<String, String> answerCache;
// 语义缓存:相似的 question → 相同的 cached answer
public String answer(String question, String playerId) {
String cacheKey = buildCacheKey(question);
String cached = answerCache.get(cacheKey);
if (cached != null) {
return cached; // cache hit — 不调 LLM
}
String answer = ragService.answerQuestion(playerId, question);
answerCache.put(cacheKey, answer);
return answer;
}
// 缓存 key 可以是问题的语义嵌入向量的哈希
private String buildCacheKey(String question) {
float[] embedding = embeddingClient.embed(question);
return hashVector(embedding); // 语义相近的问题得到相同 hash
}
}缓存策略的关键参数:
- TTL(过期时间):比赛规则一周变一次,FAQ 一月变一次。不同的 query 类型设置不同的 TTL。
- 相似度阈值:两个问题语义相似度超过 95% 才命中缓存。低了会返回不精确的答案。
- 预热:上线前把常见问题批量加载到缓存中。
第四层:评估模式——AI 系统的测试怎么搞
传统软件的测试是"输入 x + 代码 → 输出 y"。AI 系统的输出是概率性的——同样的 prompt 每次可能得到不同的回答。你不能写 assertEquals("报名成功", answer)。
评估 AI 系统的三种层次:
层次一:组件级评估 —— 每个模块独立测
// 评估检索器
@Test
void retrieverReturnsRelevantDocs() {
List<Document> docs = retriever.search("如何报名擂台赛?");
assertTrue(docs.stream().anyMatch(d -> d.getContent().contains("报名")));
}
// 评估工具调用
@Test
void registerToolAcceptsValidInput() {
String result = registerTool.execute("""
{"tournamentId": "t-1", "playerId": "p-456"}""");
assertTrue(result.contains("成功"));
}层次二:输出质量评估
// 使用 LLM 评估另一个 LLM(LLM-as-a-judge)
@Test
void answerShouldBeRelevant() {
String question = "为什么我的积分没有增加?";
String answer = ragService.answerQuestion("p-456", question);
// 用另一个 LLM 评估回答质量
String evaluation = chatClient.generate("""
问题:%s
回答:%s
请评估这个回答(0-5分):
- 是否直接回答了问题(0-5)
- 是否有用(0-5)
- 是否准确(0-5)
返回 JSON: {"relevance": 0, "usefulness": 0, "accuracy": 0}
""".formatted(question, answer));
EvaluationResult result = parseEvaluation(evaluation);
assertTrue(result.getAverage() >= 3.5, "回答质量不达标");
}层次三:生产监控评估
在生产环境中持续收集用户反馈:
// 记录用户对 AI 回答的评分
@PostMapping("/feedback")
public void recordFeedback(@RequestBody FeedbackRequest req) {
evaluationStore.save(new EvaluationRecord(
req.getQuestion(),
req.getAnswer(),
req.getUserRating(), // 点赞/点踩
req.getLatencyMs(),
System.currentTimeMillis()
));
}
// 每小时检查一次
@Scheduled(cron = "0 0 * * * *")
public void checkAISystemHealth() {
double avgRating = evaluationStore.getAverageRatingForLastHour();
if (avgRating < 3.0) {
alertService.send("AI 系统评分低于阈值:" + avgRating);
}
}常见陷阱
陷阱一:RAG 的数据新鲜度问题。 你今天更新了比赛规则,但向量数据库里的文档还是昨天的。用户得到的是旧规则。建立文档更新的 pipeline——每次文档变更,重新计算 embedding 并更新向量库。
陷阱二:Agent 给 LLM 太多工具。 30 个工具的描述全部塞在 prompt 里——LLM 开始"迷路"、选错工具。给 Agent 的工具不应该超过 10-15 个。工具描述要精简。
陷阱三:缓存结果的过时问题。 "比赛规则" 缓存了一周。这周规则改了——用户得到的是旧规则。每个缓存项需要有 TTL 和失效事件。
陷阱四:AI 系统没有 fallback。 LLM API 挂了——你的 RAG 系统返回 500。这是不可接受的。所有 AI 功能必须有 fallback 机制:LLM 不可用时,回退到预定义的 FAQ 或规则引擎。
public String safeAnswer(String question) {
try {
return ragService.answerQuestion(..., question);
} catch (LLMException e) {
return defaultAnswers.getOrDefault(question, "请稍后再试");
}
}通关挑战
- 热身:为你的系统设计一个 RAG 的"知识库"——需要索引哪些文档?(比赛规则、FAQ、版本更新说明)。画一个简单的架构图。
- 挑战(思维):设计一个 Agent 工具集——你的擂台系统可以交给 LLM 控制哪些 API?列出至少 5 个工具,每个工具写描述和参数。
- 挑战(评估):写一个 LLM-as-a-judge 的评估脚本——你定义的 10 个测试问题,运行你的 RAG 系统,让另一个 LLM 打分。计算平均分。
旅人笔记
AI 系统把软件工程带到了概率世界——RAG 给 LLM 外挂知识库,Agent 让 LLM 能调用工具,CAG 用缓存对抗延迟,评估模式让非确定性输出变得可衡量。这是软件工程的最前线——2026 年的工匠之都,AI 能力正在成为每一个系统的标配模块。
下一站预告
六卷到此完结。你从变量村出发,走过算法森林、计算机地心、网络驿道、数据库堡垒,最后在工匠之都学会了完整的工程技能——从静态检查到测试优先、重构、设计模式、架构演进、微服务、事件驱动直到 AI 系统模式。
前方的路通往 Vol 7: 分布式系统——那里是远征军的营地,林将军在等着你。你不再是一个学徒,你是带着整套工程工具箱的战士。