Skip to content

元数据卡

  • 前置知识:第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),存储起来。用户提问也被转为向量,找到最相似的文档片段。

java
// 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: "已为你报名明日锦标赛!"
java
// 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 可能持续循环(永远输入工具调用 -> 工具返回 -> 再调用工具)。必须设最大迭代次数:

java
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 的输出缓存起来——对于相同或高度相似的问题,直接返回缓存结果。

java
// 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 系统的三种层次:

层次一:组件级评估 —— 每个模块独立测

java
// 评估检索器
@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("成功"));
}

层次二:输出质量评估

java
// 使用 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, "回答质量不达标");
}

层次三:生产监控评估

在生产环境中持续收集用户反馈:

java
// 记录用户对 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 或规则引擎。

java
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: 分布式系统——那里是远征军的营地,林将军在等着你。你不再是一个学徒,你是带着整套工程工具箱的战士。

Built with VitePress | Software Systems Atlas