15-AI与智能体
什么是Agent?组成部分有哪些?
原始问法:
- 什么是Agent?组成部分有哪些?
来源题目:
SRC-15-151-448
面试先答
Agent 是一种以大语言模型(LLM)为核心推理引擎、具备自主感知、决策和行动能力的智能系统。与纯大模型对话不同,Agent 能理解用户意图、规划执行步骤、调用外部工具、获取实时信息,并根据执行结果动态调整后续行动。其核心组成包括:大模型(决策大脑)、上下文管理(记忆)、工具系统(行动能力)、规划模块(任务分解)、执行器(行动调度)。Agent 的价值在于突破了大模型"只能生成文本"的限制,使其能在真实环境中完成复杂任务,但同时也引入了不确定性、安全风险和工程复杂度。
核心结论
- Agent = LLM + 上下文 + 工具 + 规划 + 执行,是让大模型"能做事"的系统封装
- Agent 的核心能力是"感知-思考-行动"循环(Think-Act-Obsere Loop)
- Agent 的关键工程挑战在于意图理解准确性、工具调用可靠性、上下文窗口管理和安全防护
1. 是什么
Agent(智能体)是一个以 大语言模型为核心决策引擎 的自主智能系统。它具备以下五个基本组成部分:
| 组成部分 | 作用 | 说明 |
|---|---|---|
| 大模型(Brain) | 理解、推理、决策 | 负责意图识别、任务规划、工具选择和结果总结 |
| 上下文管理(Memory) | 会话历史、长期记忆 | 包括短期记忆(当前对话历史)和长期记忆(知识持久化) |
| 工具系统(Tools) | 扩展行动能力 | 通过 Function Call、MCP 等协议调用外部 API、数据库、文件系统 |
| 规划模块(Planner) | 任务分解与调度 | 将复杂目标拆解为可执行的子任务序列 |
| 执行器(Executor) | 工具调用与结果处理 | 按规划顺序调用工具,将结果反馈给大模型 |
概念边界:Agent 不同于普通的 ChatBot(仅对话)、不同于 Workflow(固定流程)、不同于 Plugin(被动调用)。Agent 是主动的、自主决策的循环系统。
2. 为什么需要它
纯大模型存在三大局限:
- 知识截止:模型训练数据有截止日期,无法获取实时信息
- 无法行动:只能生成文本,不能真正操作外部系统(下单、发邮件、查询数据库)
- 不可验证:生成内容缺乏事实校验,容易产生幻觉
Agent 通过工具调用和循环执行,解决了上述矛盾:让模型"能查到最新信息"、"能真正动手做事"、"能通过工具结果验证答案"。
3. 底层原理与完整流程
Agent 的核心是 感知-思考-行动(Think-Act-Observe)循环:
用户请求 → [感知] 理解意图
→ [思考] 规划步骤、选择工具
→ [行动] 调用工具 API
→ [观察] 获取工具执行结果
→ [思考] 判断是否完成/需要继续
→ 循环直至任务完成
详细流程:
- 接收输入:用户请求 + 当前上下文历史
- 意图识别:大模型分析用户真正想达成的目标
- 任务规划:将目标分解为有序子任务,选择所需工具
- 工具调用:通过 Function Call 协议发送调用请求
- 结果处理:接收工具返回结果,判断是否满足需求
- 循环迭代:如未完成则继续规划-调用循环
- 结束条件:大模型判断任务已达成或无法继续
4. 怎么使用
// 伪代码:使用 Agent 框架的典型方式
Agent agent = Agent.builder()
.model(new ChatModel("gpt-4o"))
.addTool(new SearchTool())
.addTool(new DatabaseQueryTool())
.addTool(new FileReadTool())
.memory(new InMemorySession())
.build();
String result = agent.run("帮我查询订单12345的状态,如果已发货就通知用户");
System.out.println(result);
5. 适用场景
- 需要多步骤推理+工具调用的复杂任务(如数据分析报告生成、自动化运维)
- 需要实时信息获取的场景(如实时股票查询、天气推荐)
- 需要操作外部系统的业务流程(如工单创建、邮件发送)
- 需要自主决策的智能体(如自主编码助手、智能客服)
6. 不适用场景与替代方案
| 场景 | 推荐方案 |
|---|---|
| 简单问答/纯知识查询 | 直接用大模型对话 |
| 固定流程业务 | 传统 Workflow/状态机 |
| 实时高并发的确定性操作 | RPC + 规则引擎 |
| 对延迟要求极高 | 同步接口调用 + 规则匹配 |
7. 优缺点与技术取舍
优点:
- 自主决策能力强,可处理开放域复杂任务
- 灵活度高,同一 Agent 可适配多种场景
- 可扩展性强,新工具即插即用
缺点:
- 不确定高,每轮循环都可能引入错误累积
- 延迟大,多轮 LLM 调用导致响应慢
- 成本高,多次 Token 消耗
- 调试困难,决策路径不透明
8. 常见问题及解决方案
| 问题 | 原因 | 解决方案 |
|---|---|---|
| Agent 死循环 | 大模型无法判断终止条件 | 设置最大轮次 + 终止条件检测 |
| 工具调用参数错误 | LLM 生成的参数格式不对 | 入参校验层 + 自动格式修正 |
| 结果偏离意图 | 规划步骤错误或工具不可用 | 增加人工确认环节 + 降级策略 |
| Token 超限 | 上下文历史过长 | 上下文压缩 + 滑动窗口截断 |
9. 版本差异与实现边界
Agent 是概念而非标准,各框架实现差异大:
- LangChain/LangGraph:基于状态图的流程编排,支持复杂 Agent 拓扑
- AutoGen:多 Agent 对话框架,侧重 Agent 间协作
- Spring AI:Java 生态的 AI 集成框架,2024 年发布 1.0
- MCP 协议:2024 年由 Anthropic 提出,标准化工具调用协议
10. 常见追问
- 追问 1:Agent 和 Workflow 的本质区别?
- Agent 是动态规划,每步决策由 LLM 实时生成;Workflow 是预定义流程,路径固定
- 追问 2:Agent 如何保证决策可解释?
- 记录每轮的思考链(CoT)、工具选择理由、终止判断依据,形成可追溯轨迹
11. 易错点
- ❌ 错误:Agent = 大模型 + 工具
- ✅ 正确:Agent 是包含大模型、上下文、工具、规划、执行五要素的完整系统
- ❌ 错误:Agent 一定比 Workflow 好
- ✅ 正确:确定性场景用 Workflow 更可靠,开放性复杂任务用 Agent
- ❌ 错误:Agent 不需要人工干预
- ✅ 正确:高风险场景必须设置人工确认节点
一句话总结
Agent 是以大模型为决策大脑、辅以上下文记忆和工具调用、通过循环推理自主完成复杂任务的智能系统。
Agent和大模型怎么交互、协同?工作流程是什么?
原始问法:
- Agent和大模型怎么交互、协同?工作流程是什么?
来源题目:
SRC-15-151-449
面试先答
Agent 与大模型的交互采用 Prompt-Response-Observation 循环模式:Agent 将用户意图、可用工具、历史上下文组装成 Prompt 发送给大模型,大模型返回文本回复或工具调用指令,Agent 解析响应、执行工具、将结果反馈给大模型,如此循环直至任务完成。二者是**"大脑"与"身体"的协同关系**:大模型负责理解、推理和决策,Agent 负责工具执行、状态管理和流程控制。这种协同的关键在于:每轮交互都是结构化的,Agent 通过 System Prompt 约束大模型的输出格式,确保可解析、可执行。
核心结论
- Agent 是大模型的"执行层",大模型是 Agent 的"决策层",二者通过结构化消息交互
- 交互协议(Function Call / Tool Use)是实现可靠协同的核心
- 完整工作流:用户输入 → Prompt 组装 → LLM 推理 → 响应解析 → 工具执行 → 结果反馈 → 循环/终止
1. 是什么
Agent 与大模型的交互是一种 "大脑-身体"协同架构:
- 大模型(大脑):负责理解意图、规划步骤、生成行动指令、总结结果
- Agent(身体):负责管理上下文、调度工具、执行操作、验证结果
二者通过结构化消息协议(如 Function Call)进行通信,每一轮交互都是:
- Agent 组装消息(用户意图 + 历史 + 工具定义)
- 大模型推理输出(文本回复 + 工具调用请求)
- Agent 执行工具并反馈结果
- 重复直至完成
2. 为什么需要它
没有 Agent 的大模型是"裸"的:
- 无法调用工具(没有执行通道)
- 无法管理多轮上下文(没有状态管理)
- 无法自主循环(没有流程控制)
没有大模型的 Agent 是"死"的:
- 无法理解自然语言意图
- 无法处理开放式决策
- 无法规划复杂任务
二者协同才构成完整的智能系统。
3. 底层原理与完整流程
┌─────────────┐ ┌──────────────┐ ┌─────────────┐
│ 1. 用户请求 │────→│ 2. Agent │────→│ 3. 大模型 │
└─────────────┘ │ 组装 Prompt │ │ 推理决策 │
└──────────────┘ └──────┬──────┘
│
┌────────▼────────┐
│ 4. 解析响应 │
│ 文本/工具调用 │
└────────┬────────┘
│
┌─────────────────┼─────────────────┐
▼ ▼
┌─────────────┐ ┌─────────────┐
│ 5a. 直接回复 │ │ 5b. 执行工具 │
└─────────────┘ └──────┬──────┘
│
┌─────────▼─────────┐
│ 6. 结果反馈给模型 │
│ 进入下一轮循环 │
└───────────────────┘
关键交互协议:Function Call 格式
{
"role": "assistant",
"content": null,
"tool_calls": [{
"id": "call_abc123",
"type": "function",
"function": {
"name": "search_order",
"arguments": "{\"order_id\": \"12345\"}"
}
}]
}
4. 怎么使用
// 伪代码:模拟 Agent-LLM 交互流程
while (!agent.isTaskComplete()) {
// Step 1: 组装消息
List<Message> messages = agent.buildMessages(userInput, history, tools);
// Step 2: 调用大模型
LLMResponse response = llm.chat(messages);
// Step 3: 解析响应
if (response.hasToolCalls()) {
// Step 4: 执行工具
ToolResult result = agent.executeTool(response.getToolCalls());
// Step 5: 反馈结果
agent.addMessage(new ToolMessage(result));
} else {
// Step 6: 直接回复用户
agent.finalize(response.getText());
}
}
5. 适用场景
- 多轮工具调用场景(如数据分析、报告生成)
- 需要大模型理解+外部执行的混合任务
- 开放式问答+操作的复合业务
6. 不适用场景与替代方案
- 简单对话场景 → 直接用大模型
- 固定流程 → Workflow 引擎
- 高频实时操作 → 同步接口 + 规则
7. 优缺点与技术取舍
优点:灵活、可扩展、能处理复杂任务 缺点:每轮交互都有 LLM 延迟和 Token 成本,交互链越长不确定性越高
8. 常见问题及解决方案
| 问题 | 解决方案 |
|---|---|
| 大模型输出格式不规范 | 通过 System Prompt 严格约束 + JSON Schema 校验 |
| 上下文过长导致 Token 超限 | 滑动窗口 + 摘要压缩 + 关键信息保留 |
| 工具调用链路过长 | 设置最大轮次限制 + 中间结果验证 |
9. 版本差异与实现边界
- 早期 Agent 框架(如 BabyAGI)基于纯 Prompt 驱动
- LangChain/LangAgent 引入了 Tool 抽象层
- 2024 年后 MCP 协议标准化了工具调用接口
- 各云厂商 Agent 平台(AWS Bedrock Agents、Azure AI Agent Service)均采用类似的交互模式
10. 常见追问
- 追问 1:如何保证每轮交互的可靠性?
- 通过响应格式约束、入参校验、工具结果验证三层保障
- 追问 2:如果大模型一直无法正确决策怎么办?
- 引入人工干预节点、设置超时降级、切换更强大模型
11. 易错点
- ❌ 错误:Agent 和大模型是同一个东西
- ✅ 正确:大模型是决策核心,Agent 是围绕大模型的执行框架
- ❌ 错误:Agent 交互只有一轮
- ✅ 正确:Agent 的核心是多轮循环交互
一句话总结
Agent 与大模型是"身体-大脑"的协同关系:Agent 管理流程和工具执行,大模型负责理解和决策,二者通过结构化消息循环交互完成复杂任务。
为什么要给大模型引用工具?单纯大模型不行吗?
原始问法:
- 为什么要给大模型引用工具?单纯大模型不行吗?
来源题目:
SRC-15-151-450
面试先答
单纯大模型存在三大硬伤:知识截止、无法行动、不可验证。给大模型引入工具就是为了突破这三大瓶颈。首先,工具让模型能获取实时信息(查天气、查数据库、查最新新闻),突破训练数据的时间限制;其次,工具让模型能真正"动手做事"(发邮件、创建工单、操作文件),从"只会说"变成"会做事";最后,工具的返回结果是客观事实,可以用来验证模型回答的正确性,减少幻觉。虽然 RAG 也能补充知识,但工具是主动行动,这是检索增强做不到的。
核心结论
- 工具扩展了大模型的三种核心能力:实时获取信息、主动执行操作、结果验证
- 单纯大模型受限于训练数据截止时间和无执行能力,无法对接真实业务
- 工具调用是 Agent 与普通 ChatBot 的本质区别
1. 是什么
给大模型引用工具,就是通过 Function Call 机制让大模型在推理过程中调用外部 API 或服务的能力。工具可以是:
- 数据查询类:数据库查询、搜索引擎、API 接口
- 操作执行类:发邮件、创建工单、文件读写
- 计算辅助类:代码执行、数学计算、格式转换
- 系统集成类:日历、消息、第三方服务对接
2. 为什么需要它
单纯大模型的三大缺陷:
| 缺陷 | 具体表现 | 影响 |
|---|---|---|
| 知识截止 | 训练数据有截止日,不知道最新发生的事 | 无法回答实时问题(如"今天的股价") |
| 无法行动 | 只能生成文本,不能操作外部系统 | 无法完成实际任务(如"帮我订会议室") |
| 不可验证 | 无法检验自己的回答是否正确 | 容易产生幻觉(如编造不存在的API) |
引入工具后的改进:
用户:"帮我查一下订单12345现在到哪了"
单纯大模型:(训练数据中没有该订单)→ 可能编造答案
引入工具:调用订单查询API → 返回真实物流信息 → 可信回答
3. 底层原理与完整流程
1. 开发者注册工具 → 定义工具名称、参数、描述
2. Agent 将工具定义以 JSON Schema 格式注入 System Prompt
3. 大模型根据用户请求判断是否需要调用工具
4. 若需要,输出 Function Call 格式的响应
5. Agent 解析响应、执行对应工具
6. 将工具结果(Tool Message)反馈给大模型
7. 大模型基于真实结果继续推理或直接回答
4. 怎么使用
// 伪代码:注册并使用工具
// 1. 定义工具
Tool orderQueryTool = Tool.builder()
.name("query_order")
.description("根据订单号查询订单状态和物流信息")
.parameter("order_id", "string", "订单编号")
.handler(params -> orderService.queryOrder(params.get("order_id")))
.build();
// 2. 注册到 Agent
agent.registerTool(orderQueryTool);
// 3. 大模型自动根据需要调用
// 用户:"查一下订单ORD-2024-001的状态"
// 大模型输出:function_call -> query_order({order_id: "ORD-2024-001"})
// Agent 执行工具,将结果返回给大模型
5. 适用场景
- 需要实时数据查询的场景
- 需要操作外部系统的业务流程
- 需要计算能力的场景(代码执行、数据处理)
- 需要跨系统集成的复杂任务
6. 不适用场景与替代方案
- 纯知识问答 → 直接大模型 + RAG
- 高频确定性调用 → 传统 RPC 接口
- 超低延迟要求 → 同步接口,不经过 LLM 决策
7. 优缺点与技术取舍
优点:突破大模型能力边界,对接真实业务 缺点:引入外部依赖增加不确定性,工具调用增加延迟和成本
8. 常见问题及解决方案
| 问题 | 解决方案 |
|---|---|
| 大模型选错工具 | 优化工具描述 + Few-shot 示例 |
| 工具参数生成错误 | 入参校验 + 自动格式修正 |
| 工具服务不可用 | 重试机制 + 降级保底 |
9. 版本差异与实现边界
- GPT-3.5 时代:Function Call 是 GPT 专属能力
- GPT-4/4o:增强了工具调用的并发能力和结构化输出
- 开源模型(如 Llama 3):也支持类似的 Tool Use 格式
- MCP 协议:2024 年标准化了工具调用的通信协议,降低了工具集成成本
10. 常见追问
- 追问 1:RAG 和工具调用有什么区别?
- RAG 是"读"预存知识,工具是"查"实时数据+做实际操作
- 追问 2:工具调用的安全性如何保障?
- 通过权限控制、参数校验、操作审计三层防护
11. 易错点
- ❌ 错误:有了工具大模型就不会产生幻觉
- ✅ 正确:工具只在被正确调用时提供验证,模型仍可能选择错误的工具或误解结果
- ❌ 错误:工具越多 Agent 能力越强
- ✅ 正确:工具过多会增加模型选择的困惑度,应按需精简
一句话总结
给大模型引入工具,是为了突破其知识截止、无法行动、不可验证的三大固有局限,使其从"只会说"的对话模型升级为"会做事"的智能体。
工具给大模型扩展了什么能力?
原始问法:
- 工具给大模型扩展了什么能力?
来源题目:
SRC-15-151-451
面试先答
工具给大模型扩展了四大核心能力:信息获取能力、行动执行能力、计算辅助能力、系统集成能力。信息获取让模型能查到实时数据(天气、股价、数据库记录),突破训练数据的时间限制;行动执行让模型能操作外部系统(发邮件、建工单、写文件),从"回答问题"变成"完成任务";计算辅助让模型能进行复杂运算(代码执行、大数计算、格式转换),弥补模型精确计算的不足;系统集成让模型能跨平台协作(对接日历、消息、第三方 API),融入现有业务生态。每种能力都对应一类具体工具,构成 Agent 的能力工具箱。
核心结论
- 工具扩展了四类能力:信息获取、行动执行、计算辅助、系统集成
- 每类能力对应不同的工具类型和使用场景
- 工具能力的选择直接影响 Agent 的实际可用性
1. 信息获取能力
让模型突破训练数据截止时间,获取实时/动态信息:
| 工具类型 | 示例 | 解决的问题 |
|---|---|---|
| 搜索引擎 | Google Search、Bing Search | 实时新闻、最新资讯 |
| 数据库查询 | MySQL、PostgreSQL 查询 | 业务数据实时查询 |
| API 接口 | 天气 API、股票 API | 第三方实时数据 |
| 文件读取 | 本地文件、云存储 | 自有文档内容 |
2. 行动执行能力
让模型从"说"变成"做",真正操作外部系统:
| 工具类型 | 示例 | 应用场景 |
|---|---|---|
| 消息发送 | 邮件、短信、钉钉通知 | 发送通知、告警 |
| 工单创建 | Jira、GitLab Issue | 项目管理 |
| 文件写入 | 创建、编辑、删除文件 | 文档生成 |
| 订单操作 | 创建/取消订单 | 业务自动化 |
3. 计算辅助能力
弥补大模型在精确计算和逻辑推理上的不足:
| 工具类型 | 示例 | 解决的问题 |
|---|---|---|
| 代码执行 | Python REPL、沙箱环境 | 复杂运算、数据处理 |
| 数学计算 | Wolfram Alpha | 精确数学、公式推导 |
| 格式转换 | JSON↔XML、Markdown↔HTML | 数据格式处理 |
| SQL 生成 | 自然语言转 SQL | 数据库操作 |
4. 系统集成能力
让模型融入企业现有技术栈:
| 工具类型 | 示例 | 应用场景 |
|---|---|---|
| 日历集成 | Google Calendar、Outlook | 会议安排 |
| 版本控制 | Git、GitHub | 代码管理 |
| 消息队列 | Kafka、RabbitMQ | 异步通知 |
| 认证系统 | OAuth、SSO | 权限管理 |
5. 底层原理
工具扩展能力的本质是 通过 Function Call 协议将大模型的文本输出转化为结构化的 API 调用:
大模型推理 → 判断需要什么能力 → 输出 Function Call
→ Agent 解析 → 执行对应工具 → 返回 Tool Result
→ 大模型基于结果继续推理
6. 适用场景
- 信息获取型:实时数据查询、资讯聚合
- 行动执行型:自动化办公、业务流程
- 计算辅助型:数据分析报告、科学计算
- 系统集成型:企业内部智能化改造
7. 常见追问
- 追问 1:如何判断该用哪种工具?
- 根据任务类型和工具描述,由大模型自动选择;或通过路由规则提前分发
- 追问 2:工具能力的边界在哪里?
- 受限于工具本身的 API 能力,Agent 只能做工具支持的事
一句话总结
工具为大模型扩展了信息获取、行动执行、计算辅助和系统集成四大能力,使其从文本生成器升级为能感知环境、执行操作、融合系统的智能体。
什么是Function Call?
原始问法:
- 什么是Function Call?
来源题目:
SRC-15-151-452
面试先答
Function Call 是大模型的一种能力,指模型在生成回复时,可以主动触发调用外部函数(工具),而不是只能生成纯文本。具体来说,模型输出的不是普通文本,而是一个结构化的函数调用请求,包含函数名和参数。Agent 框架接收到这个请求后,执行对应的函数,将执行结果作为"工具消息"反馈给模型,模型再基于结果生成最终回复。这一机制是 Agent 实现"感知-行动-观察"循环的核心技术基础,让大模型从被动回答变成主动调用外部能力。
核心结论
- Function Call 是大模型输出结构化函数调用指令的能力
- 它是 Agent 实现工具调用的核心协议
- 工作流程:模型输出调用请求 → Agent 执行函数 → 结果反馈 → 模型继续推理
1. 是什么
Function Call(函数调用)是指 大模型在生成回复时,能够输出一段结构化的"调用请求"而非普通文本,请求调用外部预注册的函数/工具。
// Function Call 响应示例
{
"role": "assistant",
"content": null,
"function_call": {
"name": "get_weather",
"arguments": "{\"city\": \"北京\", \"date\": \"2024-06-01\"}"
}
}
核心要素:
- 函数名:预注册的工具标识
- 参数:JSON 格式的调用参数
- 调用 ID:用于匹配结果
2. 为什么需要它
没有 Function Call,大模型只能输出文本。开发者需要从文本中解析出调用意图和参数,这非常不可靠。Function Call 将"调用请求"标准化为结构化格式,实现了:
- 可靠的意图识别(无需 NLP 解析自由文本)
- 规范的参数传递(JSON Schema 校验)
- 可预测的交互流程(Agent 可程序化处理)
3. 底层原理与完整流程
Step 1: 开发者定义工具的 JSON Schema
↓
Step 2: Agent 将 Schema 注入 System Prompt
↓
Step 3: 大模型推理时,根据 Schema 生成函数调用
↓
Step 4: Agent 检测到 function_call 字段
↓
Step 5: Agent 执行对应函数,获取结果
↓
Step 6: Agent 将结果封装为 Tool Message 返回
↓
Step 7: 大模型基于结果继续推理
4. 怎么使用
// 伪代码:Function Call 的使用方式
// 1. 定义工具 Schema
String toolSchema = """
{
"name": "get_weather",
"description": "获取指定城市的天气",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "城市名称"},
"date": {"type": "string", "description": "日期 YYYY-MM-DD"}
},
"required": ["city"]
}
}
""";
// 2. 调用大模型时传入工具列表
ChatCompletionResponse response = client.chat(
messages,
List.of(toolSchema) // 注册可用工具
);
// 3. 处理响应
if (response.hasFunctionCall()) {
FunctionCall call = response.getFunctionCall();
String result = executeFunction(call);
// 4. 将结果反馈给模型
messages.add(new ToolMessage(call.getId(), result));
// 再次调用模型...
}
5. 适用场景
- 需要大模型自主选择调用外部 API 的场景
- 多轮工具调用的复杂任务
- 需要结构化输出与程序交互的场景
6. 不适用场景与替代方案
- 固定流程的 API 调用 → 硬编码调用
- 大模型不需要判断意图 → 路由规则直接分发
- 简单问答 → 不需要 Function Call
7. 优缺点与技术取舍
优点:
- 结构化输出,可靠可解析
- 模型自主选择调用,灵活度高
- 标准化协议,跨平台兼容
缺点:
- 依赖模型理解 Schema 的能力
- 每轮调用增加延迟
- 并发调用管理复杂
8. 常见问题及解决方案
| 问题 | 解决方案 |
|---|---|
| 参数格式错误 | 严格 JSON Schema + 自动修正 |
| 模型不调用工具 | 在 Prompt 中强化使用说明 + Few-shot 示例 |
| 并发调用冲突 | 串行执行 + 结果汇总 |
9. 版本差异与实现边界
- GPT 3.5 Turbo:首次支持 Function Call
- GPT 4/4o:支持并发多工具调用、增强了参数理解能力
- Claude 3:支持 Tool Use 协议
- 开源模型:Llama 3、Mistral 等也支持类似格式
- MCP 协议:2024 年标准化了工具调用的通信协议
10. 常见追问
- 追问 1:Function Call 和普通 Prompt 中有什么区别?
- Function Call 输出是结构化的函数调用,普通 Prompt 输出是自然语言文本
- 追问 2:如何设计好的工具 Schema?
- 清晰的描述(让模型知道何时调用)、精确的参数定义、合理的必填字段
11. 易错点
- ❌ 错误:Function Call = 调用 Java 函数
- ✅ 正确:Function Call 是大模型的输出格式,Agent 框架解析后执行对应操作
- ❌ 错误:Function Call 一定会被正确触发
- ✅ 正确:模型可能不调用或错误调用,需要 Prompt 工程引导
一句话总结
Function Call 是大模型输出结构化函数调用请求的能力,是 Agent 实现"自主决策+工具执行"的核心机制。
什么是MCP?MCP的组成是什么?
原始问法:
- 什么是MCP?MCP的组成是什么?
来源题目:
SRC-15-151-453
面试先答
MCP(Model Context Protocol)是 Anthropic 公司在 2024 年提出的一套开放协议,用于标准化大模型与外部工具/数据源之间的通信方式。可以把它理解为 AI 时代的"USB 接口标准"——像 USB 统一了外设连接一样,MCP 统一了大模型与工具的接入方式。MCP 由三个核心组件构成:MCP Client(AI 应用端)、MCP Server(工具提供方)、MCP Protocol(通信协议)。Client 消费 Server 提供的能力,Protocol 定义了双方的消息格式和交互规范。它的价值在于:一套标准接入所有工具,避免了每个 AI 平台都要单独适配的碎片化问题。
核心结论
- MCP 是 AI 时代的工具/数据接入标准协议,由 Anthropic 2024 年发起
- 核心三组件:MCP Client、MCP Server、MCP Protocol
- 解决了多平台工具碎片化对接问题,降低了 AI 应用集成成本
1. 是什么
MCP(Model Context Protocol) 是一个开放协议,定义了大模型应用(Client)与外部工具/数据提供者(Server)之间的标准化通信方式。
设计目标:
- 一套标准,接入所有 AI 平台
- 工具提供方只需实现一次,就能被所有支持 MCP 的应用使用
- 类似 HTTP 对 Web 的意义,MCP 致力于成为 AI 应用的"通用连接器"
核心组成:
| 组件 | 角色 | 说明 |
|---|---|---|
| MCP Client | 消费者 | AI 应用(如 Claude Desktop、Cursor),连接 Server 并使用其能力 |
| MCP Server | 提供者 | 将外部工具/数据封装为 MCP 服务,暴露给 Client 使用 |
| MCP Protocol | 协议层 | 定义 Client-Server 间的消息格式、能力发现、调用规范 |
2. 为什么需要它
没有 MCP 的问题:
- 碎片化:每个 AI 平台有自己的工具接入协议,开发者需要重复适配
- 成本高:同一工具要为不同 AI 平台开发多个版本
- 扩展难:新 AI 平台出现时,现有工具无法直接复用
MCP 的解决方案:
- 标准化:一次实现,多端复用
- 解耦:工具提供方和 AI 应用方独立演进
- 生态化:形成 AI 工具的开放生态
3. 底层原理与完整流程
┌──────────────┐ MCP Protocol ┌──────────────┐
│ MCP Client │◄────────────────────►│ MCP Server │
│ (AI 应用) │ │ (工具提供方) │
└──────────────┘ └──────────────┘
通信流程:
1. Client → Server: initialize(握手,协商版本)
2. Client → Server: tools/list(获取可用工具列表)
3. Server → Client: 返回工具定义(名称、描述、参数 Schema)
4. Client → Server: tools/call(调用指定工具)
5. Server → Client: 返回执行结果
6. Client → Server: resources/list(获取可用资源)
7. Server → Client: 返回资源列表
8. Client → Server: resources/read(读取资源)
4. 怎么使用
// 伪代码:实现一个 MCP Server
// 1. 创建 MCP Server
MCPServer server = MCPServer.builder()
.name("weather-server")
.version("1.0.0")
.build();
// 2. 注册工具
server.registerTool("get_weather", ToolDef.builder()
.description("获取城市天气")
.parameter("city", "string", "城市")
.handler(params -> weatherService.getWeather(params.get("city")))
.build());
// 3. 注册资源
server.registerResource("weather_codes",
ResourceDef.builder()
.description("天气代码表")
.mimeType("application/json")
.content(weatherCodesJson)
.build());
// 4. 启动 Server
server.start(); // 通过 stdio/SSE 与 Client 通信
5. 适用场景
- 为 AI 应用提供外部工具/数据的开发者
- 需要对接多个 AI 平台的企业
- AI 工具生态的建设者
- 企业内部 AI 能力的标准化输出
6. 不适用场景与替代方案
- 仅对接单一 AI 平台 → 可使用平台原生协议
- 内部工具仅自用 → 无需 MCP 标准
- 超低延迟要求 → MCP 协议有额外开销
7. 优缺点与技术取舍
优点:
- 一套标准,多端复用
- 开放生态,社区驱动
- 解耦 Client 和 Server,独立演进
缺点:
- 协议仍在演进,可能有 breaking changes
- 相比直接对接有额外协议开销
- 社区生态还在发展中
8. 常见问题及解决方案
| 问题 | 解决方案 |
|---|---|
| Client 不支持 MCP | 通过适配桥接(Adapter Bridge)转换协议 |
| Server 实现复杂 | 使用官方 SDK(支持 Python/TypeScript/Java) |
| 工具调用安全 | MCP 支持权限控制和操作审计 |
9. 版本差异与实现边界
- 2024 年 6 月:Anthropic 发布 MCP 协议规范
- 当前版本:Protocol Version 2024-11-05
- 传输层:支持 stdio 和 SSE(Server-Sent Events)两种传输方式
- 语言支持:官方 SDK 支持 TypeScript、Python、Java
- 兼容性:Claude Desktop、Cursor 等已原生支持 MCP
10. 常见追问
- 追问 1:MCP 和 Function Call 的关系?
- Function Call 是模型端的能力,MCP 是工具端的协议,二者互补
- 追问 2:MCP 的安全机制?
- 支持身份认证、权限控制、操作审计、沙箱隔离
11. 易错点
- ❌ 错误:MCP 是 Anthropic 的私有协议
- ✅ 正确:MCP 是开放协议,任何组织和个人都可参与
- ❌ 错误:MCP 只能用于 Claude
- ✅ 正确:MCP 独立于模型厂商,任何 AI 应用都可支持
一句话总结
MCP 是 AI 时代的"USB 标准接口",由 Client、Server、Protocol 三组件构成,统一了大模型与外部工具/数据的接入方式。
什么是Skill?和Function Call、MCP的区别是什么?
原始问法:
- 什么是Skill?和Function Call、MCP的区别是什么?
来源题目:
SRC-15-151-454
面试先答
Skill 是 Agent 框架中的一个 高层封装概念,指一组相关能力的集合,通常包含 Prompt 模板、工具调用流程和处理逻辑。可以把 Skill 理解为 Agent 的"技能包"——就像人的技能一样,一个 Skill 可能包含多个步骤和多种工具的组合调用。而 Function Call 是单步的"函数调用指令",MCP 是工具间通信的"协议标准"。三者的关系是:Function Call 是原子调用,MCP 是连接标准,Skill 是封装了多个调用的复合能力。打个比方:Function Call 是"一个按钮",MCP 是"USB 接口",Skill 是"整个厨房料理台"。
核心结论
- Skill 是 Agent 的高层封装,包含 Prompt + 工具 + 逻辑的复合能力单元
- Function Call 是原子调用,单步的函数调用指令
- MCP 是通信协议,标准化的工具接入规范
1. 是什么
Skill(技能) 是 Agent 框架中对一组相关能力的高层封装,通常包含:
- System Prompt 片段:定义该 Skill 的行为规范
- 工具定义:该 Skill 可使用的工具集合
- 执行逻辑:调用这些工具的流程和规则
- 示例:Few-shot 示例,引导模型正确使用
Skill 示例:"数据分析技能"
├── Prompt:你是一个数据分析专家,擅长处理 CSV 文件...
├── 工具:[文件读取、Python 执行、图表生成]
└── 逻辑:读取数据 → 清洗 → 分析 → 生成报告
2. 三者对比
| 维度 | Function Call | MCP | Skill |
|---|---|---|---|
| 定位 | 原子调用指令 | 通信协议标准 | 高层能力封装 |
| 粒度 | 单步函数调用 | Client-Server 交互 | 多步复合能力 |
| 关注点 | 调用格式 | 传输规范 | 业务语义 |
| 使用者 | Agent 框架 | 工具开发者 | Agent 设计者 |
| 类比 | 一个按钮 | USB 接口 | 整个功能模块 |
3. 底层原理与完整流程
Skill 内部可以包含多个 Function Call 和 MCP 工具:
Skill "订单处理"
├── Function Call: query_order(order_id) ← 原子调用
├── MCP Tool: send_email(to, content) ← 通过 MCP 调用
├── Function Call: create_ticket(description) ← 原子调用
└── 执行逻辑:查询 → 通知 → 建单(步骤编排)
4. 怎么使用
// 伪代码:定义和使用 Skill
// 定义 Skill
Skill dataAnalysisSkill = Skill.builder()
.name("data_analysis")
.systemPrompt("你是数据分析专家...")
.tools(List.of(fileReadTool, pythonExecTool, chartTool))
.build();
// 使用 Skill
agent.registerSkill(dataAnalysisSkill);
// 用户:"帮我分析这个销售数据文件"
// Agent 加载 data_analysis Skill,使用其中的工具完成分析
5. 适用场景
- 将常用业务流程封装为可复用能力
- 为不同领域的 Agent 提供专业技能包
- 降低 Agent 使用门槛,开箱即用
6. 优缺点与技术取舍
优点:复用性强、语义清晰、降低使用门槛 缺点:封装过度会增加灵活性,Skill 间可能存在能力重叠
7. 常见追问
- 追问 1:Skill 和 Tool 的关系?
- Skill 是 Tool 的编排组合,一个 Skill 可以使用多个 Tool
- 追问 2:Skill 是否可以跨 Agent 共享?
- 可以,Skill 是独立封装,可以在不同 Agent 间复用
一句话总结
Skill 是 Agent 的"技能包",封装了 Prompt、工具和执行逻辑的复合能力;Function Call 是单步调用指令,MCP 是工具通信标准,三者对应 Agent 系统的不同抽象层级。
MCP和Skill哪个上下文占用大?
原始问法:
- MCP和Skill哪个上下文占用大?
来源题目:
SRC-15-151-455
面试先答
Skill 的上下文占用通常比 MCP 大得多。原因是 Skill 需要将完整的 Prompt 模板、工具定义、执行逻辑和示例代码都注入到 Agent 的上下文窗口中,一个 Skill 可能消耗数千甚至上万 Token。而 MCP 本身只是一个通信协议,它的上下文占用主要是工具的元信息(名称、描述、参数 Schema),这些通常只有几百 Token。换句话说,Skill 是"预加载"到上下文的完整能力包,而 MCP 只是"按需调用"的轻量接口描述。不过,MCP 调用后返回的结果也会占用上下文,这取决于工具返回数据的大小。
核心结论
- Skill 上下文占用大(Prompt + 工具定义 + 逻辑 + 示例)
- MCP 上下文占用小(仅工具元信息)
- 实际项目中应按需选择:Skill 适合固定场景,MCP 适合动态调用
1. 详细分析
Skill 的上下文占用:
一个 Skill 的上下文组成:
├── System Prompt: ~500-2000 Tokens
├── 工具定义(JSON Schema): ~200-500 Tokens/工具
├── Few-shot 示例: ~500-2000 Tokens
└── 执行逻辑描述: ~200-500 Tokens
总计:一个 Skill 通常占用 1500-5000 Tokens
MCP 的上下文占用:
MCP 工具的上下文组成:
├── 工具名称: ~10 Tokens
├── 工具描述: ~50-200 Tokens
├── 参数 Schema: ~50-200 Tokens
└── (仅此而已)
总计:一个 MCP 工具通常占用 100-500 Tokens
2. 设计权衡
| 维度 | Skill | MCP |
|---|---|---|
| 上下文占用 | 大 | 小 |
| 能力范围 | 复合能力 | 单一能力 |
| 调用方式 | Skill 激活即加载 | 按需加载 |
| 适用场景 | 固定流程、高频使用 | 动态调用、低频使用 |
| 灵活性 | 较低(固定封装) | 高(按需组合) |
3. 最佳实践
- Skill 适合:高频使用的固定业务流程(如数据分析、报告生成)
- MCP 适合:低频调用的外部工具(如天气查询、数据库操作)
- 混合策略:Skill 内部通过 MCP 调用外部工具,既保持 Skill 的封装性,又利用 MCP 的灵活性
4. 常见追问
- 追问 1:如何优化 Skill 的上下文占用?
- 精简 Prompt、按需加载工具、使用动态 Skill 切换
- 追问 2:MCP 工具返回结果太大怎么办?
- 对结果进行截断、摘要或分页处理
一句话总结
Skill 上下文占用大(封装了完整能力包),MCP 上下文占用小(仅工具元信息),实际项目中应根据使用频率和场景灵活选择。
Agent怎么判断某轮tool call后该不该结束?
原始问法:
- Agent怎么判断某轮tool call后该不该结束?
来源题目:
SRC-15-151-456
面试先答
Agent 判断一轮 Tool Call 后该不该结束,主要有 四种机制:一是 大模型自主判断,在 System Prompt 中要求模型在完成目标时输出特定的结束标记(如 <COMPLETE>),由 Agent 框架检测该标记;二是 显式结束条件,通过代码预设判断规则(如检查工具返回结果是否包含关键字段);三是 最大轮次限制,设置一个上限防止死循环;四是 人工确认节点,在关键步骤后强制要求用户确认。实际项目中通常组合使用这四种机制,既保证任务完成度,又防止无限循环。
核心结论
- Agent 结束判断的四种核心机制:LLM 自主判断、显式条件、最大轮次、人工确认
- 通常组合使用多种机制确保可靠性
- 设计核心是平衡"完成度"和"效率"
1. 是什么
Agent 的结束判断是指在一轮 Tool Call 完成后,系统决定是继续执行下一轮还是终止任务的过程。这是 Agent 循环的关键控制节点。
2. 四种结束判断机制
| 机制 | 实现方式 | 优点 | 缺点 |
|---|---|---|---|
| LLM 自主判断 | Prompt 引导模型输出结束标记 | 灵活,适应开放场景 | 可能判断错误 |
| 显式条件 | 代码检查结果是否满足预设规则 | 确定性强 | 只能覆盖已知场景 |
| 最大轮次 | 设置循环上限(如 10 轮) | 防止死循环 | 可能提前终止未完成任务 |
| 人工确认 | 关键节点暂停等待用户 | 安全可控 | 用户体验受影响 |
3. 底层原理与完整流程
Tool Call 返回结果
↓
┌─────────────────────┐
│ 1. LLM 分析结果 │ ──→ 输出"继续"或"结束"
└─────────┬───────────┘
│
▼
┌─────────────────────┐
│ 2. 检查显式条件 │ ──→ 结果是否满足预设规则
└─────────┬───────────┘
│
▼
┌─────────────────────┐
│ 3. 检查最大轮次 │ ──→ 是否已达上限
└─────────┬───────────┘
│
▼
┌─────────────────────┐
│ 4. 是否需要人工确认 │ ──→ 关键节点暂停
└─────────┬───────────┘
│
┌────┴────┐
▼ ▼
继续执行 结束任务
4. 怎么使用
// 伪代码:实现多机制结束判断
int maxRounds = 10;
int currentRound = 0;
while (currentRound < maxRounds) {
currentRound++;
// 执行 Tool Call
ToolResult result = agent.executeNextTool();
// 机制 1: LLM 自主判断
if (result.containsCompletionMark()) {
break;
}
// 机制 2: 显式条件
if (result.matches(completionRule)) {
break;
}
// 机制 3: 最大轮次
if (currentRound >= maxRounds) {
agent.finalize("已达到最大执行轮次");
break;
}
// 机制 4: 人工确认
if (result.needsUserConfirmation()) {
String userInput = agent.waitForUserInput();
if (userInput.equals("确认完成")) break;
}
}
5. 适用场景
- 开放式任务:LLM 自主判断 + 最大轮次
- 确定性流程:显式条件 + 最大轮次
- 高风险操作:强制人工确认 + 其他机制
6. 常见问题及解决方案
| 问题 | 解决方案 |
|---|---|
| Agent 死循环 | 最大轮次 + 超时机制 |
| 过早结束 | 显式条件检查 + LLM 结果验证 |
| 过晚结束(浪费 Token) | 设置最小完成条件 + LLM 结果分析 |
7. 易错点
- ❌ 错误:Agent 可以精准判断何时结束
- ✅ 正确:结束判断需要多种机制组合使用,单一机制不够可靠
- ❌ 错误:只要 LLM 判断结束就一定正确
- ✅ 正确:LLM 可能误判,需要显式条件和人工确认兜底
一句话总结
Agent 通过 LLM 自主判断、显式条件、最大轮次、人工确认四种机制组合判断结束时机,在任务完成度和执行效率间取得平衡。
Agent生成代码执行用什么方法?了解过sandbox吗?
原始问法:
- Agent生成代码执行用什么方法?了解过sandbox吗?
来源题目:
SRC-15-151-457
面试先答
Agent 生成代码后不能直接在生产环境执行,通常通过 沙箱(Sandbox) 机制进行安全执行。沙箱是一个隔离的运行环境,具有资源限制(CPU、内存、执行时间)、权限控制(文件访问、网络访问受限)和安全防护(防止恶意代码执行)。常见实现包括:Docker 容器、FireJail、NSJail、E2B 等。执行流程是:Agent 将生成的代码发送到沙箱 → 沙箱在受限环境中执行 → 返回执行结果 → Agent 基于结果继续推理。沙箱的核心价值在于在安全可控的条件下赋予 Agent 代码执行能力,是 Coding Agent 的关键基础设施。
核心结论
- Agent 代码执行依赖沙箱机制,核心实现包括 Docker、FireJail、E2B 等
- 沙箱提供资源隔离、权限控制和安全防护
- 代码执行流程:生成→沙箱执行→结果返回→后续推理
1. 是什么
沙箱(Sandbox) 是一个安全隔离的代码执行环境,具有以下核心特征:
| 特征 | 说明 |
|---|---|
| 资源限制 | CPU 时间、内存大小、磁盘空间限制 |
| 权限控制 | 文件系统、网络、系统调用受限 |
| 安全隔离 | 进程级/系统级隔离,防止恶意代码扩散 |
| 超时机制 | 防止死循环或长时间占用资源 |
2. 为什么需要它
直接执行大模型生成的代码极其危险:
- 代码可能包含恶意指令(删除文件、窃取数据)
- 代码可能有 Bug(死循环、资源耗尽)
- 代码可能访问敏感资源(数据库、密钥)
沙箱通过隔离和限制,将风险降到可控范围。
3. 常见沙箱实现
| 实现 | 原理 | 特点 |
|---|---|---|
| Docker 容器 | 操作系统级虚拟化 | 隔离性强,启动较慢(秒级) |
| FireJail | Linux Namespace + Cgroup | 轻量级,毫秒级启动 |
| NSJail | FreeBSD 沙箱 | 系统级隔离 |
| E2B | 云端 SDK 沙箱 | 远程执行,易于扩展 |
| 本地解释器 | Python/Node REPL | 最简单,隔离性最弱 |
4. 底层原理与完整流程
Agent 生成代码
↓
发送到沙箱服务
↓
┌──────────────────────────────┐
│ 沙箱启动(容器/进程隔离) │
│ ↓ │
│ 资源限制应用(CPU/内存/超时) │
│ ↓ │
│ 权限控制(文件/网络/系统调用) │
│ ↓ │
│ 代码执行 │
│ ↓ │
│ 捕获输出/异常 │
│ ↓ │
│ 销毁沙箱环境 │
└──────────────────────────────┘
↓
返回执行结果给 Agent
5. 怎么使用
// 伪代码:使用沙箱执行代码
Sandbox sandbox = Sandbox.builder()
.type(SandboxType.E2B)
.timeout(30, TimeUnit.SECONDS)
.memoryLimit(512, MemoryUnit.MB)
.cpuLimit(1.0)
.allowNetwork(false)
.allowFileAccess("/tmp/workspace")
.build();
ExecutionResult result = sandbox.execute(code, language);
if (result.isSuccess()) {
System.out.println("Output: " + result.getOutput());
} else {
System.out.println("Error: " + result.getError());
}
6. 适用场景
- Coding Agent 的代码执行和验证
- 数据分析 Agent 的代码运行
- 需要动态执行代码的自动化场景
- AI 辅助编程的实时反馈
7. 常见追问
- 追问 1:沙箱如何保证安全?
- 多层防护:进程隔离 + 资源限制 + 权限控制 + 安全审计
- 追问 2:沙箱执行的代码能访问生产数据库吗?
- 通常不行,沙箱网络受限;如有需要通过受控的 MCP 代理间接访问
一句话总结
Agent 通过沙箱安全执行生成的代码,沙箱提供资源隔离、权限控制和安全防护,是 Coding Agent 等需要动态代码执行场景的关键基础设施。
Agent执行代码工具如何设计?报错了怎么办?
原始问法:
- Agent执行代码工具如何设计?报错了怎么办?
来源题目:
SRC-15-151-458
面试先答
设计 Agent 的代码执行工具需要考虑 五个核心维度:一是 安全隔离(通过沙箱限制资源和权限),二是 多语言支持(对接不同编程语言的解释器),三是 结果捕获(标准输出、错误输出、执行状态的完整采集),四是 错误处理(将代码报错转化为可被 Agent 理解的结构化信息),五是 重试与降级(代码出错时自动修正或降级处理)。报错后的处理策略包括:将错误信息结构化反馈给 LLM 让其自行修正、设置重试次数上限、超过阈值则降级为手动处理、记录错误日志用于后续分析改进。
核心结论
- 代码执行工具设计五维度:安全、多语言、结果捕获、错误处理、重试降级
- 报错处理:结构化反馈→LLM 自愈→重试→降级→人工
- 关键设计原则:可恢复、可观测、可降级
1. 工具架构设计
┌─────────────────────────────────────────────────┐
│ Code Execution Tool │
├─────────────────────────────────────────────────┤
│ 1. 安全层(沙箱) │
│ ├── Docker / FireJail / E2B │
│ ├── 资源限制(CPU/内存/超时) │
│ └── 权限控制(文件/网络/系统调用) │
├─────────────────────────────────────────────────┤
│ 2. 执行层(多语言) │
│ ├── Python 解释器 │
│ ├── Node.js 运行时 │
│ ├── Java (JDK) │
│ └── Shell/Bash │
├─────────────────────────────────────────────────┤
│ 3. 采集层(结果捕获) │
│ ├── 标准输出(stdout) │
│ ├── 错误输出(stderr) │
│ ├── 执行状态(exit code) │
│ └── 执行耗时(timing) │
├─────────────────────────────────────────────────┤
│ 4. 处理层(错误处理) │
│ ├── 错误分类(语法/运行时/资源) │
│ ├── 结构化错误信息生成 │
│ └── 重试/降级决策 │
├─────────────────────────────────────────────────┤
│ 5. 观测层(日志与监控) │
│ ├── 执行日志记录 │
│ ├── 错误统计分析 │
│ └── 性能监控告警 │
└─────────────────────────────────────────────────┘
2. 报错处理策略
代码执行报错
↓
┌─────────────────────┐
│ 1. 错误分类 │
│ ├── 语法错误 │ → 反馈给 LLM 修正
│ ├── 运行时错误 │ → 反馈给 LLM 修正
│ ├── 资源超限 │ → 调整资源限制或简化代码
│ └── 沙箱异常 │ → 基础设施问题,告警
└─────────┬───────────┘
│
▼
┌─────────────────────┐
│ 2. 结构化错误反馈 │
│ { │
│ "error_type": │
│ "message": │
│ "line": │
│ "suggestion": │
│ } │
└─────────┬───────────┘
│
▼
┌─────────────────────┐
│ 3. LLM 自愈(重试) │ → 最多 N 次(默认 3)
└─────────┬───────────┘
│
▼
重试成功?──否──→ 降级处理
│是
▼
返回结果 ├── 简化执行
├── 建议用户手动
└── 记录学习(用于 Prompt 优化)
3. 怎么使用
// 伪代码:代码执行工具使用
CodeExecutionTool codeTool = CodeExecutionTool.builder()
.sandbox(new DockerSandbox("python:3.11-slim"))
.language(ProgrammingLanguage.PYTHON)
.maxRetries(3)
.timeout(30, TimeUnit.SECONDS)
.onError(ErrorStrategy.AUTO_FIX)
.build();
ExecutionResult result = codeTool.execute(code);
// result 包含: success, stdout, stderr, exitCode, executionTime
if (!result.isSuccess() && result.getRetryCount() >= 3) {
// 降级处理
agent.handoffToUser("代码多次执行失败,建议手动检查");
}
4. 常见追问
- 追问 1:如何设计"自愈"机制?
- 将错误信息结构化后反馈给 LLM,Prompt 中明确要求其修正代码后重试
- 追问 2:如何防止无限重试?
- 设置最大重试次数 + 重试次数递增等待 + 失败后降级
一句话总结
代码执行工具通过安全沙箱、多语言执行、结果采集、错误处理和重试降级五层架构设计,报错后通过"分类→结构化反馈→LLM 自愈→降级"的策略实现可靠执行。
重试做在tool内还是agent多次调用?
原始问法:
- 重试做在tool内还是agent多次调用?
来源题目:
SRC-15-151-459
面试先答
重试机制的选择取决于 失败原因和重试策略。一般来说,瞬时故障(如网络抖动、超时)应在 Tool 内部重试,因为这属于基础设施层面的问题,不需要 LLM 介入判断;而 逻辑错误(如参数错误、业务规则不满足)应由 Agent 通过多次调用解决,因为这需要 LLM 理解错误原因并调整参数。最佳实践是 分层重试:Tool 内部处理瞬时故障(固定次数、固定间隔),Agent 层面处理逻辑错误(LLM 分析原因、动态调整方案)。关键原则是:能在 Tool 层解决的不上升到 Agent 层,需要 LLM 智能判断的不在 Tool 层硬编码。
核心结论
- 瞬时故障在 Tool 内重试,逻辑错误由 Agent 多次调用解决
- 采用分层重试策略:基础设施问题底层解决,智能决策问题交给 Agent
- 关键区分:是否需要 LLM 的智能判断能力
1. 分层重试策略
失败场景分析:
├── 基础设施层(Tool 内部重试)
│ ├── 网络超时/抖动
│ ├── 服务暂时不可用
│ └── 资源临时不可用
│
│ 处理:固定次数(2-3次)+ 递增等待(1s, 2s, 4s)
│
└── 智能层(Agent 多次调用)
├── 参数格式错误
├── 业务规则不满足
├── 需要补充信息
└── 服务返回需要进一步处理
│
处理:LLM 分析错误 → 调整参数/方案 → 重新调用
2. 对比分析
| 维度 | Tool 内重试 | Agent 多次调用 |
|---|---|---|
| 决策方 | Tool 框架(规则) | LLM(智能判断) |
| 适用场景 | 瞬时故障 | 逻辑错误 |
| Token 消耗 | 无额外消耗 | 消耗 Token |
| 延迟 | 低(毫秒级) | 高(秒级) |
| 灵活性 | 固定策略 | 动态调整 |
| 典型实现 | Spring Retry、Resilience4j | Agent 循环 + LLM 推理 |
3. 最佳实践
// 伪代码:分层重试设计
// Tool 层:处理瞬时故障
@Retryable(
value = {TimeoutException.class, ServiceUnavailableException.class},
maxAttempts = 3,
backoff = @Backoff(delay = 1000, multiplier = 2)
)
public ToolResult callTool(ToolRequest request) {
return toolClient.execute(request);
}
// Agent 层:处理逻辑错误
while (retryCount < MAX_AGENT_RETRIES) {
ToolResult result = tool.execute(request);
if (result.isLogicError()) {
// 需要 LLM 分析并调整
String correctedParams = llm.analyzeAndFix(
result.getError(),
request.getParams()
);
request.setParams(correctedParams);
retryCount++;
} else if (result.isSuccess()) {
break;
} else {
// 未知错误,直接失败
throw new AgentExecutionException(result.getError());
}
}
4. 常见追问
- 追问 1:如果 Tool 内重试也失败了怎么办?
- 抛出异常给 Agent,由 Agent 决定是否进一步重试或降级
- 追问 2:如何避免重试风暴?
- 设置全局重试上限 + 熔断器(失败率过高时自动停止)
一句话总结
瞬时故障在 Tool 内重试解决(固定策略、零 Token 成本),逻辑错误由 Agent 通过多次调用解决(LLM 智能判断、有 Token 成本),二者分层协作实现高效可靠的重试机制。
如果一轮tool call返回结果非常多,怎么设计?
原始问法:
- 如果一轮tool call返回结果非常多,怎么设计?
来源题目:
SRC-15-151-460
面试先答
当 Tool Call 返回结果非常多时(如返回上万条数据库记录、超长文本文件),需要 四种策略组合处理:一是 结果分页,Tool 支持分页参数,Agent 按需获取后续页面;二是 摘要截断,Tool 返回前 N 条或摘要信息,由 Agent 判断是否需要更多;三是 按需检索,Tool 提供过滤/搜索接口,Agent 通过精细化查询缩小范围;四是 结果缓存,将大结果存储在外部,仅返回引用 ID,Agent 需要时再按需获取。核心原则是 避免将大结果直接注入 LLM 上下文,通过分层获取和按需访问控制 Token 消耗。
核心结论
- 大结果处理四策略:分页、摘要截断、按需检索、结果缓存
- 核心原则:不将大结果直接注入 LLM 上下文
- 需要 Tool 设计时就考虑结果的分层返回能力
1. 设计方案
策略一:分页返回
// Tool 支持分页参数
ToolResult result = tool.queryOrders(
QueryRequest.builder()
.pageSize(20) // 每页大小
.pageNum(1) // 当前页
.filter("status=paid")
.build()
);
// Agent 根据结果决定是否翻页
if (result.hasMore()) {
// 获取下一页
ToolResult nextPage = tool.queryOrders(result.nextPageRequest());
}
策略二:摘要 + 详情
// Tool 返回摘要和详情引用
ToolResult result = tool.search("订单数据");
// result.getSummary() → "共找到 1,234 条订单,前 20 条已返回"
// result.getTotalCount() → 1234
// result.getDetailRef() → "ref:order_detail_abc" (引用 ID)
// Agent 需要详情时再通过 ref 获取
ToolResult detail = tool.getDetail("ref:order_detail_abc");
策略三:按需检索
// Tool 提供精细化查询接口
ToolResult result = tool.queryOrders(
QueryRequest.builder()
.filter("status=paid AND amount>1000")
.sortBy("create_time", SortOrder.DESC)
.fields("order_id", "status", "amount") // 只返回需要的字段
.build()
);
策略四:结果缓存
// 大结果存储在外部,仅返回引用
ToolResult result = tool.exportData("orders_2024");
// result.getCacheId() → "cache_abc123"
// result.getExpireTime() → "2024-06-02T10:00:00"
// Agent 需要时通过引用 ID 异步获取
2. 完整处理流程
Tool 返回大结果
↓
┌─────────────────────┐
│ 1. 判断结果量级 │
│ ├── 小(<1000 Token)→ 直接返回
│ ├── 中(1000-10K)→ 摘要截断
│ └── 大(>10K)→ 分页/缓存
└─────────┬───────────┘
│
▼
┌─────────────────────┐
│ 2. 返回分层结果 │
│ ├── 摘要/前 N 条 │
│ ├── 总数/分页信息 │
│ └── 引用 ID │
└─────────┬───────────┘
│
▼
┌─────────────────────┐
│ 3. Agent 按需获取 │
│ ├── 需要更多 → 翻页│
│ ├── 需要详情 → 引用│
│ └── 不需要 → 结束 │
└─────────────────────┘
3. 常见追问
- 追问 1:如何设计 Tool 的结果返回能力?
- 预留分页参数、支持字段选择、提供摘要模式、结果可缓存
- 追问 2:缓存的结果怎么清理?
- 设置过期时间 + 访问统计 + 定期清理机制
一句话总结
大结果处理通过分页、摘要截断、按需检索、结果缓存四策略组合,核心原则是避免将大结果直接注入 LLM 上下文。
Coding agent会有哪些模块?
原始问法:
- Coding agent会有哪些模块?
来源题目:
SRC-15-151-461
面试先答
Coding Agent 通常包含 七大核心模块:一是 需求理解模块,解析用户意图、识别编程任务类型;二是 代码规划模块,将任务分解为编码步骤、选择技术方案;三是 代码生成模块,基于规划生成代码片段或完整文件;四是 代码执行模块,在沙箱中运行代码验证正确性;五是 错误修复模块,分析执行错误并自动修正代码;六是 项目管理模块,管理文件结构、依赖关系、代码风格;七是 交互协作模块,与用户沟通、确认关键决策。这些模块协同工作,形成"理解→规划→生成→执行→修复→交付"的完整编码流水线。
核心结论
- Coding Agent 七大模块:需求理解、代码规划、代码生成、代码执行、错误修复、项目管理、交互协作
- 核心流水线:理解→规划→生成→执行→修复→交付
- 关键能力:自主规划 + 代码生成 + 自动修复 + 项目感知
1. 模块架构
┌─────────────────────────────────────────────────┐
│ Coding Agent 架构 │
├─────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ 需求理解模块 │──→│ 代码规划模块 │ │
│ └─────────────┘ └──────┬──────┘ │
│ │ │
│ ▼ │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ 项目管理模块 │──→│ 代码生成模块 │ │
│ └─────────────┘ └──────┬──────┘ │
│ │ │
│ ▼ │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ 错误修复模块 │←──│ 代码执行模块 │ │
│ └─────────────┘ └─────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────┐ │
│ │ 交互协作模块 │ │
│ └─────────────┘ │
│ │
└─────────────────────────────────────────────────┘
2. 各模块详解
| 模块 | 核心职责 | 关键技术 |
|---|---|---|
| 需求理解 | 解析用户意图、识别任务类型 | NLU、任务分类、上下文分析 |
| 代码规划 | 分解任务、选择技术方案 | 任务分解、技术选型、依赖分析 |
| 代码生成 | 生成代码片段/文件 | LLM 代码生成、模板引擎、AST 操作 |
| 代码执行 | 编译/运行/测试代码 | 沙箱环境、构建工具、测试框架 |
| 错误修复 | 分析错误、修正代码 | 错误分类、LLM 修正、自动补丁 |
| 项目管理 | 文件/依赖/风格管理 | 文件系统、包管理器、Lint 工具 |
| 交互协作 | 与用户沟通、确认决策 | 对话界面、确认节点、进度展示 |
3. 典型工作流
用户:"帮我写一个 Spring Boot 的用户 CRUD 接口"
↓
需求理解:识别为 Web API 开发任务
↓
代码规划:
1. 创建 User 实体类
2. 创建 UserRepository
3. 创建 UserService
4. 创建 UserController
5. 配置数据库连接
6. 编写单元测试
↓
代码生成:按规划逐步生成各文件
↓
项目管理:创建文件结构、添加依赖
↓
代码执行:编译 + 运行 + 测试
↓
错误修复:编译错误 → 修正 → 重新执行
↓
交互协作:向用户展示结果、确认是否需要调整
4. 常见追问
- 追问 1:Coding Agent 如何保证代码风格一致?
- 通过代码风格配置(如 EditorConfig)+ Lint 工具 + Few-shot 示例引导
- 追问 2:Coding Agent 能处理多大规模的项目?
- 取决于上下文窗口和规划能力,当前适合单文件/单模块级别,大规模项目需多 Agent 协作
一句话总结
Coding Agent 由需求理解、代码规划、代码生成、代码执行、错误修复、项目管理、交互协作七大模块组成,形成完整的自动化编码流水线。
多个agents怎么相互协作?
原始问法:
- 多个agents怎么相互协作?
来源题目:
SRC-15-151-462
面试先答
多个 Agent 协作主要有 三种模式:编排模式(Orchestration)、对话模式(Conversation) 和 层级模式(Hierarchy)。编排模式由一个主控 Agent 分配任务给子 Agent,适合流程明确的场景;对话模式中 Agent 之间平等对话、协商解决问题,适合开放域讨论;层级模式建立上下级关系,上级 Agent 监督下级 Agent 执行。实现关键包括 Agent 间通信协议(消息传递、共享黑板)、任务分配策略(按能力/负载分配)、冲突解决机制(投票、优先级、仲裁)。典型案例是 AutoGen 的多 Agent 对话框架和 MetaGPT 的角色扮演协作。
核心结论
- 多 Agent 协作三模式:编排(主控分配)、对话(平等协商)、层级(上下级管理)
- 关键技术:通信协议、任务分配、冲突解决
- 适用场景:复杂项目、多领域协作、并行处理
1. 三种协作模式
| 模式 | 架构 | 适用场景 | 代表框架 |
|---|---|---|---|
| 编排模式 | 主控 Agent → 子 Agent | 流程明确的任务 | LangGraph、CrewAI |
| 对话模式 | Agent ↔ Agent 平等对话 | 开放域讨论、协商 | AutoGen、ChatDev |
| 层级模式 | 上级 Agent → 下级 Agent | 大项目分级管理 | MetaGPT、CAMEL |
2. 编排模式详解
┌─────────────────────────────────┐
│ Orchestrator Agent │
│ (主控:任务分配、结果汇总) │
└────────┬──────────┬──────────────┘
│ │
┌────▼───┐ ┌───▼────┐ ┌────────┐
│Agent A │ │Agent B │ │Agent C │
│(前端) │ │(后端) │ │(测试) │
└────────┘ └────────┘ └────────┘
│ │ │
└──────────┴──────────┘
│
结果汇总
3. 对话模式详解
┌──────────┐ 提议 ┌──────────┐
│ Agent A │──────→│ Agent B │
│(产品经理)│ │(架构师) │
└────┬─────┘ └────┬─────┘
│ │
│ ←──── 反对 ────│
│ │
▼ ▼
┌──────────┐ 协商 ┌──────────┐
│ Agent C │──────→│ Agent D │
│(开发者) │ │(测试) │
└──────────┘ └──────────┘
│ │
└──────── 达成共识 ────────┘
4. 通信机制
方案一:消息传递
agentA.sendMessage(agentB,
Message.builder()
.type(MessageType.TASK_REQUEST)
.content("请实现用户认证接口")
.build()
);
方案二:共享黑板
// 所有 Agent 共享一个知识黑板
blackboard.post("task_board",
TaskBoard.builder()
.addTask("auth_api", Status.IN_PROGRESS)
.addResult("auth_api", "完成 JWT 认证实现")
.build()
);
// Agent 从黑板读取其他 Agent 的进度
5. 冲突解决
| 冲突类型 | 解决策略 |
|---|---|
| 资源竞争 | 优先级分配 + 队列调度 |
| 意见分歧 | 投票表决 + 仲裁机制 |
| 任务重叠 | 去重检测 + 任务拆分 |
| 结果不一致 | 交叉验证 + 人工介入 |
6. 常见追问
- 追问 1:多 Agent 协作的效率如何?
- 并行处理可提升效率,但通信开销和协调成本也会增加
- 追问 2:如何避免多 Agent 之间的冲突?
- 通过明确的角色定义、任务边界、冲突解决机制组合保障
一句话总结
多 Agent 通过编排、对话、层级三种模式协作,核心技术是通信协议、任务分配和冲突解决,适用于复杂项目的并行开发和多领域协作。
Agent间通信和状态管理怎么设计?
原始问法:
- Agent间通信和状态管理怎么设计?
来源题目:
SRC-15-151-463
面试先答
多 Agent 系统的通信和状态管理需要 一套完整的设计方案。通信层面主要有两种模式:消息传递(Agent 间直接发送结构化消息,支持异步和同步)和 共享黑板(所有 Agent 读写同一个共享状态空间,实现间接通信)。状态管理层面需要解决 状态持久化(会话/任务状态存储)、状态同步(多 Agent 间状态一致性)、状态隔离(不同任务/会话的状态隔离)三个核心问题。推荐的设计是:用消息传递做实时通信、用共享黑板做状态共享、用 Redis/数据库做状态持久化,三者组合使用。
核心结论
- 通信模式:消息传递(直接通信)+ 共享黑板(间接通信)
- 状态管理三要素:持久化、同步、隔离
- 推荐组合:消息传递 + 共享黑板 + 持久化存储
1. 通信设计
消息传递模式
// Agent 间直接消息传递
MessageBus messageBus = new MessageBus();
// 发送消息
agent.send(Message.builder()
.id(UUID.randomUUID().toString())
.from("agent-001")
.to("agent-002")
.type(MessageType.TASK_RESULT)
.content("用户模块开发完成")
.payload(Map.of("files", List.of("User.java", "UserController.java")))
.timestamp(LocalDateTime.now())
.build());
// 接收消息
messageBus.subscribe("agent-002", message -> {
if (message.getType() == MessageType.TASK_RESULT) {
// 处理任务结果
}
});
共享黑板模式
// 所有 Agent 共享的状态空间
Blackboard blackboard = new Blackboard();
// Agent A 写入状态
blackboard.write("task:user-module",
TaskState.builder()
.status(Status.IN_PROGRESS)
.progress(0.6)
.artifacts(List.of("User.java"))
.updatedAt(LocalDateTime.now())
.build()
);
// Agent B 读取状态
TaskState state = blackboard.read("task:user-module");
if (state.getProgress() > 0.5) {
// 开始测试准备
}
两种模式对比
| 维度 | 消息传递 | 共享黑板 |
|---|---|---|
| 通信方式 | 点对点直接通信 | 读写间接通信 |
| 实时性 | 高(实时推送) | 中(需轮询或通知) |
| 可追溯性 | 需记录消息 | 天然可追溯 |
| 复杂度 | 高(需寻址和路由) | 低(统一入口) |
| 适用场景 | 实时协作、指令传递 | 状态共享、进度同步 |
2. 状态管理设计
状态持久化
// 使用 Redis 持久化 Agent 状态
StateStore stateStore = RedisStateStore.builder()
.host("localhost")
.port(6379)
.keyPrefix("agent:state:")
.ttl(24, TimeUnit.HOURS)
.build();
// 保存状态
stateStore.save("session-001", AgentState.builder()
.sessionId("session-001")
.currentTask("用户认证模块")
.completedTasks(List.of("实体创建", "Repository"))
.context(Map.of("tech_stack", "Spring Boot"))
.build()
);
// 恢复状态
AgentState state = stateStore.load("session-001");
状态同步
// 状态变更通知机制
public class StateSyncManager {
private final List<StateListener> listeners = new CopyOnWriteArrayList<>();
public void registerListener(String agentId, StateListener listener) {
listeners.add(listener);
}
public void broadcast(StateChangeEvent event) {
// 通知所有相关 Agent
listeners.stream()
.filter(l -> l.isInterestedIn(event.getChannel()))
.forEach(l -> l.onStateChange(event));
}
}
状态隔离
// 按 session_id 隔离状态
public class IsolatedStateManager {
private final Map<String, AgentState> sessionStates = new ConcurrentHashMap<>();
public AgentState getState(String sessionId) {
return sessionStates.computeIfAbsent(sessionId,
id -> AgentState.init(id)
);
}
public void updateState(String sessionId, AgentState state) {
sessionStates.put(sessionId, state);
}
public void cleanExpired() {
// 清理过期会话
}
}
3. 完整架构
┌─────────────────────────────────────────────────────┐
│ 通信与状态管理架构 │
├─────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ Agent A │ │ Agent B │ │
│ └──────┬───────┘ └──────┬───────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌──────────────────────────────────────┐ │
│ │ 消息传递层(Message Bus) │ │
│ │ 点对点通信 / 广播 / 订阅 │ │
│ └──────────────────────────────────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌──────────────────────────────────────┐ │
│ │ 共享黑板层(Blackboard) │ │
│ │ 状态共享 / 进度同步 / 结果汇总 │ │
│ └──────────────────────────────────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌──────────────────────────────────────┐ │
│ │ 状态持久层(State Store) │ │
│ │ Redis / DB / 文件存储 │ │
│ └──────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────┘
4. 常见追问
- 追问 1:如何处理状态冲突?
- 通过乐观锁(版本号)、分布式锁、状态合并策略解决
- 追问 2:Agent 崩溃后如何恢复状态?
- 从持久化存储(Redis/DB)加载状态,恢复到崩溃前的检查点
一句话总结
多 Agent 通信采用消息传递+共享黑板组合模式,状态管理通过持久化、同步、隔离三要素保障,形成可靠的协同工作体系。
Agent效果评估怎么做?
原始问法:
- Agent效果评估怎么做?
来源题目:
SRC-15-151-464
面试先答
Agent 效果评估需要 从五个维度构建完整的评估体系:任务完成度(Agent 是否成功完成用户目标,最核心指标)、过程质量(决策路径是否合理、工具调用是否正确、步数是否最优)、响应质量(生成内容的准确性、相关性、完整性)、效率指标(Token 消耗、执行时间、成本)、安全指标(是否产生有害输出、是否违规操作)。评估方法结合 自动评估(定义评估指标和规则,批量执行评估用例)和 人工评估(对开放性任务进行人工打分)。常用评估框架包括 LangSmith、Langfuse、ARES 等。
核心结论
- Agent 评估五维度:任务完成度、过程质量、响应质量、效率指标、安全指标
- 评估方法:自动评估 + 人工评估结合
- 关键挑战:开放性任务的"完成"定义模糊、过程质量难以量化
1. 评估维度体系
| 维度 | 指标 | 说明 |
|---|---|---|
| 任务完成度 | 成功率、目标达成率 | 是否完成用户指定的任务 |
| 过程质量 | 工具调用准确率、步数合理性、决策正确性 | 执行过程是否高效正确 |
| 响应质量 | 答案准确性、信息相关性、格式规范性 | 最终输出的质量 |
| 效率指标 | Token 消耗、执行时间、API 调用次数 | 成本和效率 |
| 安全指标 | 违规操作数、有害输出率、权限控制合规性 | 安全性 |
2. 核心评估指标详解
任务完成度(最核心)
// 评估 Agent 是否完成任务
public class TaskCompletionEvaluator {
public EvaluationResult evaluate(AgentTask task, AgentResult result) {
boolean completed = checkTaskCompletion(task, result);
List<String> missingSteps = findMissingSteps(task, result);
List<String> errors = findErrors(result);
return EvaluationResult.builder()
.taskId(task.getId())
.completed(completed)
.completionRate(calculateCompletionRate(task, result))
.missingSteps(missingSteps)
.errors(errors)
.score(calculateScore(completed, missingSteps, errors))
.build();
}
private double calculateScore(boolean completed,
List<String> missing, List<String> errors) {
double score = completed ? 1.0 : 0.5;
score -= missing.size() * 0.1;
score -= errors.size() * 0.15;
return Math.max(0, score);
}
}
过程质量评估
public class ProcessQualityEvaluator {
public ProcessEvaluation evaluate(List<AgentStep> steps) {
int correctToolCalls = countCorrectToolCalls(steps);
int totalToolCalls = steps.size();
double toolAccuracy = totalToolCalls > 0
? (double) correctToolCalls / totalToolCalls
: 0.0;
int optimalSteps = calculateOptimalSteps(steps);
int actualSteps = steps.size();
double stepEfficiency = optimalSteps > 0
? (double) optimalSteps / actualSteps
: 1.0;
return ProcessEvaluation.builder()
.toolCallAccuracy(toolAccuracy)
.stepEfficiency(stepEfficiency)
.decisionQuality(evaluateDecisions(steps))
.build();
}
}
效率指标评估
public class EfficiencyEvaluator {
public EfficiencyMetrics evaluate(AgentExecution execution) {
return EfficiencyMetrics.builder()
.totalTokens(execution.getTotalTokens())
.inputTokens(execution.getInputTokens())
.outputTokens(execution.getOutputTokens())
.toolResultTokens(execution.getToolResultTokens())
.executionTimeMs(execution.getDuration().toMillis())
.toolCallCount(execution.getToolCalls().size())
.estimatedCost(calculateCost(execution))
.build();
}
}
3. 评估方法
自动评估
- 定义评估用例(输入 + 预期输出/行为)
- 设定评估规则(关键词匹配、格式校验、API 调用验证)
- 批量执行并统计指标
- 适用于:结构化任务(如 API 调用、数据查询)
人工评估
- 对 Agent 输出进行人工打分
- 评估维度:有用性、准确性、完整性、创造性
- 使用打分标准(1-5 分)
- 适用于:开放性任务(如写作、分析)
混合评估
自动评估(量化指标)
↓
人工评估(质量判断)
↓
加权汇总 → 综合评分
4. 评估框架
| 框架 | 特点 | 适用场景 |
|---|---|---|
| LangSmith | LangChain 官方,深度集成 | LangChain 项目 |
| Langfuse | 开源、多框架支持 | 多框架混合项目 |
| ARES | 专注 LLM 评估 | LLM 输出质量评估 |
| Ragas | RAG 专用评估 | RAG 系统评估 |
| 自建 | 定制化评估 | 特殊业务需求 |
5. 评估流程
1. 定义评估用例集
├── 覆盖典型场景(60%)
├── 覆盖边界场景(30%)
└── 覆盖异常场景(10%)
↓
2. 批量执行 Agent
↓
3. 自动评估
├── 检查任务完成度
├── 统计效率指标
├── 检测安全问题
↓
4. 人工评估
├── 抽样评估质量
├── 开放性问题打分
↓
5. 汇总报告
├── 整体评分
├── 各维度得分
├── 改进建议
6. 常见追问
- 追问 1:开放性任务如何评估完成度?
- 通过多维度验证:结果合理性、用户满意度、关键要素覆盖度
- 追问 2:如何构建评估用例集?
- 从真实用户需求出发,覆盖典型/边界/异常场景,定期更新
7. 易错点
- ❌ 错误:Agent 评估只看结果
- ✅ 正确:需要同时评估结果、过程、效率、安全
- ❌ 错误:评估只靠人工
- ✅ 正确:自动评估量化指标 + 人工评估质量判断相结合
一句话总结
Agent 效果评估从任务完成度、过程质量、响应质量、效率指标、安全指标五维度展开,通过自动量化评估和人工质量评估结合构建完整的评估体系。
RAG和传统搜索的区别是什么?
原始问法:
- RAG和传统搜索的区别是什么?
来源题目:
SRC-15-152-465
面试先答
RAG(Retrieval-Augmented Generation,检索增强生成)与传统搜索的核心区别在于 "搜索"和"生成"的融合方式。传统搜索(如 Elasticsearch、Google Search)基于关键词匹配返回文档列表,由用户自行阅读理解;RAG 则是先检索相关文档,再将检索结果作为上下文提交给大模型,由模型 基于检索内容直接生成答案。本质区别有三:一是传统搜索返回的是"文档",RAG 返回的是"答案";二是传统搜索依赖关键词精确匹配,RAG 用语义向量进行相似度匹配,能理解自然语言的深层含义;三是传统搜索不经过大模型,RAG 将检索与生成深度融合,答案更精准、更符合上下文需求。
核心结论
- RAG = 检索(Retrieval)+ 生成(Generation),检索为生成提供上下文
- 传统搜索返回文档列表,RAG 返回基于检索内容的生成答案
- RAG 用语义向量检索替代关键词匹配,理解能力更强
1. 对比分析
| 维度 | 传统搜索 | RAG |
|---|---|---|
| 核心流程 | 关键词匹配 → 返回文档列表 | 语义检索 → 大模型生成答案 |
| 检索方式 | 倒排索引、BM25、关键词匹配 | 向量嵌入、余弦相似度、混合检索 |
| 输出形式 | 文档列表(需用户阅读) | 直接答案(已整合总结) |
| 语义理解 | 依赖关键词精确匹配 | 理解自然语言深层含义 |
| 可解释性 | 返回原文,用户自行判断 | 引用来源,可追溯 |
| 适用场景 | 文档查找、关键词定位 | 知识问答、智能客服、报告生成 |
2. 流程对比
传统搜索流程:
用户输入 → 关键词提取 → 倒排索引匹配 → 返回文档列表 → 用户阅读
RAG 流程:
用户问题 → 向量化 → 语义检索 → 返回相关文档
→ 将文档+问题提交给 LLM → LLM 生成答案 + 引用来源
3. RAG 的核心优势
用户问:"如何配置 Spring Boot 的数据库连接池?"
传统搜索:
→ 返回 10 篇文档("Spring Boot HikariCP 配置"、"数据库连接池详解"...)
→ 用户需逐一阅读找到答案
RAG:
→ 检索到最相关的 3 段内容
→ 大模型生成:
"Spring Boot 默认使用 HikariCP 作为连接池,配置方式如下:
在 application.yml 中设置 spring.datasource.hikari.maximum-pool-size=10...
(引用:Spring Boot 官方文档第 8.3 节)"
4. 常见追问
- 追问 1:RAG 能否完全替代传统搜索?
- 不能。RAG 适合问答场景,传统搜索在文档查找、关键词定位等场景仍有优势
- 追问 2:RAG 的检索和生成哪个更重要?
- 同等重要。检索质量决定了答案的"证据"质量,生成质量决定了答案的"表达"质量
一句话总结
RAG 将语义检索和大模型生成深度融合,用"检索+生成"的方式直接给出答案,而传统搜索仅返回文档列表供用户自行查找。
为什么不直接用关键词检索?
原始问法:
- 为什么不直接用关键词检索?
来源题目:
SRC-15-152-466
面试先答
关键词检索(如 BM25、倒排索引)在精确匹配场景下表现优秀,但在 大模型问答场景下存在四大不足:一是 语义鸿沟问题,用户的自然语言提问可能用了与文档不同的表达方式,关键词匹配无法命中;二是 召回不完整,同义词、近义词、相关概念的文档无法被检索到;三是 排序不合理,仅靠词频排序无法衡量文档与问题的实际语义相关性;四是 无理解能力,关键词检索不理解文档内容,无法将检索结果整合成答案。在 RAG 场景下,语义检索通过向量嵌入将文本映射到高维空间,通过向量相似度匹配实现语义级别的召回,有效解决了这些问题。
核心结论
- 关键词检索在语义理解、召回完整性、排序合理性上存在不足
- 语义检索(向量检索)通过嵌入映射解决了语义鸿沟问题
- RAG 场景下关键词检索应与语义检索结合使用(混合检索)
1. 关键词检索的局限
局限一:语义鸿沟
用户问:"如何优化 Java 程序的启动速度?"
关键词检索匹配的文档:
✅ "Java 启动性能调优"(精确匹配)
❌ "Spring Boot 容器初始化优化"(相关但关键词不同)
❌ "JVM 类加载机制详解"(相关但关键词不同)
❌ "减少 Bean 初始化时间"(相关但表达方式不同)
局限二:同义词问题
用户问:"分布式锁的实现方式?"
关键词检索可能遗漏:
❌ "分布式互斥锁"(同义词"互斥锁"≠"锁")
❌ "Redis SETNX 实现并发控制"(用"并发控制"代替"锁")
❌ "Zookeeper 临时节点原理"(技术方案名称不含"锁")
局限三:排序问题
查询:"Redis 缓存穿透解决方案"
排序靠前的文档:
1. "Redis 缓存穿透、击穿、雪崩详解"(词频高,但内容不聚焦)
2. "布隆过滤器实战"(词频低,但恰好是解决方案)
→ 用户可能错过最相关的文档
2. 语义检索如何解决
语义检索通过 Embedding 将文本映射到高维向量:
"如何优化 Java 程序的启动速度?" → 向量 A
"Spring Boot 容器初始化优化" → 向量 B(与 A 余弦相似度高)
"JVM 类加载机制详解" → 向量 C(与 A 余弦相似度中等)
"减少 Bean 初始化时间" → 向量 D(与 A 余弦相似度高)
通过向量相似度计算,即使关键词不同,语义相近的文档也能被召回
3. 最佳实践:混合检索
// 混合检索:关键词 + 语义
public List<Document> hybridSearch(String query) {
// 关键词检索(保留精确匹配能力)
List<ScoredDoc> keywordResults = keywordSearch(query, topK);
// 语义检索(补充语义召回)
List<ScoredDoc> semanticResults = semanticSearch(query, topK);
// 融合排序(RRF 或加权融合)
return reciprocalRankFusion(keywordResults, semanticResults);
}
4. 常见追问
- 追问 1:那关键词检索还有用吗?
- 有用。在精确匹配、专有名词、代码片段等场景仍是首选,应与语义检索结合
- 追问 2:如何评估检索质量?
- 使用 Recall@K、MRR、NDCG 等指标,结合人工标注评估
一句话总结
关键词检索无法解决语义鸿沟、同义词和排序问题,在 RAG 场景下应使用语义检索或混合检索来提升召回质量。
RAG混合检索策略怎么做?
原始问法:
- RAG混合检索策略怎么做?
来源题目:
SRC-15-152-467
面试先答
RAG 混合检索是将 关键词检索(精确匹配)和语义检索(向量相似度)相结合 的检索策略,目的是兼取两者之长。具体实现有三种方式:串行融合(先关键词后语义或反之)、并行融合(两路检索同时进行,结果合并排序)和 加权融合(为不同检索方式分配权重)。常用的融合算法包括 RRF(Reciprocal Rank Fusion)、Combined Score 和 学习排序(LTR)。最佳实践是先用 Dense Retrieval(语义)做主要召回,用 BM25(关键词)补充精确匹配,通过 RRF 合并结果。
核心结论
- 混合检索 = 关键词检索 + 语义检索,互补优势
- 融合策略:串行、并行、加权
- 推荐算法:RRF(Reciprocal Rank Fusion)
1. 三种融合方式
串行融合
用户查询
↓
关键词检索(精确匹配)
↓ 未命中
语义检索(补充召回)
↓
返回结果
并行融合
用户查询
↓
┌──────────┬──────────┐
│ │ │
▼ ▼ ▼
关键词 语义 元数据过滤
检索 检索 (时间/权限)
│ │ │
└──────────┴──────────┘
↓
融合排序
↓
返回结果
加权融合
// RRF 融合算法
public double rrfScore(int rank, double k) {
return 1.0 / (k + rank);
}
public List<Document> hybridSearch(String query, double keywordWeight, double semanticWeight) {
List<ScoredDoc> keywordResults = keywordSearch(query);
List<ScoredDoc> semanticResults = semanticSearch(query);
// RRF 融合
Map<String, Double> scores = new HashMap<>();
for (int i = 0; i < keywordResults.size(); i++) {
String docId = keywordResults.get(i).getId();
scores.merge(docId, rrfScore(i + 1, 60) * keywordWeight, Double::sum);
}
for (int i = 0; i < semanticResults.size(); i++) {
String docId = semanticResults.get(i).getId();
scores.merge(docId, rrfScore(i + 1, 60) * semanticWeight, Double::sum);
}
return scores.entrySet().stream()
.sorted(Map.Entry.<String, Double>comparingByValue().reversed())
.map(e -> getDocument(e.getKey()))
.collect(Collectors.toList());
}
2. 融合算法对比
| 算法 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| RRF | 排名倒数求和 | 简单有效,无需训练 | 不考虑原始分数 |
| Combined Score | 分数加权求和 | 利用原始分数信息 | 需分数归一化 |
| LTR | 机器学习排序 | 最精确 | 需要训练数据 |
| CSS | 条件概率融合 | 理论完善 | 实现复杂 |
3. 调优策略
- 权重调整:根据场景调整关键词和语义的权重(如法律文本侧重关键词,客服对话侧重语义)
- 动态切换:简单查询用关键词,复杂查询用语义,混合查询用融合
- 多粒度检索:同时检索段落级和文档级粒度,提供更精准的上下文
4. 常见追问
- 追问 1:RRF 中的 k 参数如何选择?
- 通常取 60(Cormac 等人论文推荐),可根据数据集微调
- 追问 2:混合检索一定比纯语义好吗?
- 不一定。在强语义匹配场景(如概念定义),纯语义可能更优;在精确匹配场景(如报错信息),关键词更优
一句话总结
混合检索通过并行或串行方式融合关键词检索和语义检索的结果,结合 RRF 等融合算法兼取精确匹配和语义理解的优势。
RAG召回效果如何评估?
原始问法:
- RAG召回效果如何评估?
来源题目:
SRC-15-152-468
面试先答
RAG 召回效果评估需要 从检索质量和生成质量两个层面 进行。检索质量的核心指标包括 Recall@K(前 K 个结果中命中相关文档的比例,最关键的召回指标)、Precision@K(前 K 个结果中相关文档的比例)、MRR(第一个相关文档的排名倒数均值)、NDCG(考虑多相关性等级的排序质量)。生成质量的评估关注答案的准确性、完整性和引用正确性。评估方法是:先准备标注好的"查询-相关文档"数据集,运行检索系统获取结果,计算上述指标,与基线方法对比。
核心结论
- 检索质量指标:Recall@K、Precision@K、MRR、NDCG
- 生成质量指标:答案准确性、完整性、引用正确性
- 评估流程:标注数据集 → 运行检索 → 计算指标 → 对比分析
1. 检索质量指标
| 指标 | 公式 | 含义 | 关注点 |
|---|---|---|---|
| Recall@K | |相关文档∩TopK| / |相关文档| | 召回率 | 不漏文档 |
| Precision@K | |相关文档∩TopK| / K | 精确率 | 少错文档 |
| MRR | 1/|Q| × Σ 1/rank_i | 平均倒数排名 | 首个相关文档位置 |
| NDCG@K | DCG/IDCG | 归一化折损累计增益 | 整体排序质量 |
2. 评估实现
public class RetrievalEvaluator {
public EvaluationResult evaluate(List<Query> queries,
RetrievalSystem system) {
double totalRecall = 0;
double totalPrecision = 0;
double totalMRR = 0;
for (Query query : queries) {
List<Document> results = system.search(query.getText(), 10);
Set<String> relevantIds = query.getRelevantDocIds();
int relevantFound = 0;
for (int i = 0; i < results.size(); i++) {
if (relevantIds.contains(results.get(i).getId())) {
relevantFound++;
totalMRR += 1.0 / (i + 1);
}
}
totalRecall += (double) relevantFound / relevantIds.size();
totalPrecision += (double) relevantFound / results.size();
}
int n = queries.size();
return EvaluationResult.builder()
.recallAtK(totalRecall / n)
.precisionAtK(totalPrecision / n)
.mrr(totalMRR / n)
.build();
}
}
3. 生成质量评估
| 维度 | 评估方法 | 说明 |
|---|---|---|
| 答案准确性 | 人工打分 + 自动评估 | 答案是否正确 |
| 答案完整性 | 关键点覆盖率 | 是否覆盖所有要点 |
| 引用正确性 | 引用文档是否相关且正确 | 可追溯性 |
| 幻觉检测 | 判断是否有超出检索内容的生成 | 忠实度 |
4. 评估流程
Step 1: 准备标注数据集
├── 查询集合(覆盖典型/边界/异常场景)
├── 每个查询的相关文档标注
└── 每个查询的标准答案(可选)
↓
Step 2: 运行检索系统
├── 执行所有查询
├── 记录 TopK 结果
└── 保存检索日志
↓
Step 3: 计算检索指标
├── Recall@K、Precision@K
├── MRR、NDCG
└── 与基线方法对比
↓
Step 4: 评估生成质量
├── 答案准确性打分
├── 引用正确性检查
└── 幻觉检测
↓
Step 5: 生成评估报告
5. 常见追问
- 追问 1:没有标注数据怎么办?
- 使用 LLM 自动标注、人工标注小样本后扩展、使用弱监督方法
- 追问 2:Recall@K 和 Precision@K 如何取舍?
- 取决于场景:问答系统侧重 Recall(宁可多召回),搜索侧重 Precision
一句话总结
RAG 召回效果通过 Recall@K、Precision@K、MRR、NDCG 等检索指标和答案准确性等生成指标进行系统化评估,核心是构建标注数据集并运行量化指标计算。
Chunk切片粒度太大/太小有什么问题?
原始问法:
- Chunk切片粒度太大/太小有什么问题?
来源题目:
SRC-15-152-469
面试先答
Chunk 切片粒度的选择直接影响 RAG 系统的检索质量和答案质量。粒度过大会导致:一是检索精度下降,大 Chunk 包含过多无关内容,稀释了核心语义;二是上下文浪费,每个 Chunk 占用大量 Token,限制了返回的文档数量;三是噪声干扰,大段无关信息干扰大模型生成答案。粒度过小会导致:一是语义不完整,一个完整的语义单元被切碎,检索到的片段缺乏上下文;二是召回困难,小片段的向量表示不稳定,相似度匹配不准确;三是拼接成本,需要额外逻辑将多个小片段拼接。理想的 Chunk 粒度是 语义完整+长度适中,通常 200-500 Token。
核心结论
- Chunk 粒度需平衡语义完整性和检索精度
- 过大:语义稀释、Token 浪费、噪声干扰
- 过小:语义破碎、召回不准、拼接困难
- 理想粒度:200-500 Token,保持语义单元完整
1. 粒度影响分析
| 粒度 | 检索精度 | 语义完整 | Token 效率 | 典型问题 |
|---|---|---|---|---|
| 过大(>1000 Token) | 低 | 高 | 低 | 噪声干扰、上下文浪费 |
| 适中(200-500 Token) | 高 | 高 | 高 | 需按语义边界切分 |
| 过小(<100 Token) | 低 | 低 | 高 | 语义破碎、召回不准 |
2. 切片策略
固定长度切片
// 简单但可能切断语义
public List<Chunk> fixedChunk(String text, int chunkSize, int overlap) {
List<Chunk> chunks = new ArrayList<>();
for (int i = 0; i < text.length(); i += chunkSize - overlap) {
int end = Math.min(i + chunkSize, text.length());
chunks.add(new Chunk(text.substring(i, end)));
}
return chunks;
}
语义切片(推荐)
public List<Chunk> semanticChunk(String text) {
List<Chunk> chunks = new ArrayList<>();
List<String> paragraphs = splitByParagraphs(text);
StringBuilder currentChunk = new StringBuilder();
for (String para : paragraphs) {
if (currentChunk.length() + para.length() > MAX_CHUNK_SIZE) {
chunks.add(new Chunk(currentChunk.toString()));
currentChunk = new StringBuilder(para);
} else {
currentChunk.append(para);
}
}
if (currentChunk.length() > 0) {
chunks.add(new Chunk(currentChunk.toString()));
}
return chunks;
}
层级切片
文档 → 章节 → 段落 → 句子
(按文档组织结构分层切片,检索时可选择粒度)
3. 常见追问
- 追问 1:代码文件如何切片?
- 按函数/类切片,保持代码的语法完整性
- 追问 2:表格数据如何切片?
- 按行切片或将表格转为文本描述后切片
一句话总结
Chunk 切片需在语义完整性和检索精度间取得平衡,推荐 200-500 Token 的语义切片,过大会引入噪声,过小会破坏语义。
RAG系统文档频繁更新,向量索引与原文不一致怎么办?
原始问法:
- RAG系统文档频繁更新,向量索引与原文不一致怎么办?
来源题目:
SRC-15-152-470
面试先答
RAG 系统中向量索引与原文不一致是常见问题,特别是在文档频繁更新的场景下。解决方案是 建立索引与原文的联动更新机制,主要包括三种策略:同步更新(文档变更时立即触发向量重新嵌入和索引更新)、异步更新(通过消息队列或定时任务批量更新)、增量更新(只更新变更的 Chunk,而非全量重建)。关键设计包括:为每个 Chunk 维护版本号/哈希值用于变更检测、使用消息队列解耦文档变更与索引更新、设置定期全量校验任务。同时需要考虑最终一致性,允许短暂的不一致窗口。
核心结论
- 三种更新策略:同步、异步、增量
- 关键机制:版本号检测、消息队列解耦、定期校验
- 设计原则:最终一致性优先,避免强一致性的性能代价
1. 更新策略对比
| 策略 | 实现 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 同步更新 | 文档变更时立即更新索引 | 实时一致 | 影响性能 | 低频更新场景 |
| 异步更新 | 消息队列/定时任务 | 解耦、高性能 | 短暂不一致 | 中高频更新 |
| 增量更新 | 只更新变更 Chunk | 高效 | 实现复杂 | 高频更新 |
2. 系统设计
文档变更 → 版本号/哈希变更检测
↓
┌─────────────────────────────┐
│ 变更检测层 │
│ 对比文档版本号/哈希值 │
└───────────┬─────────────────┘
│
▼
┌─────────────────────────────┐
│ 更新调度层 │
│ 同步/异步/增量策略选择 │
└───────────┬─────────────────┘
│
▼
┌─────────────────────────────┐
│ 索引更新层 │
│ 重新嵌入 + 向量索引更新 │
└───────────┬─────────────────┘
│
▼
┌─────────────────────────────┐
│ 校验层(定期全量校验) │
│ 比对原文与索引的版本一致性 │
└─────────────────────────────┘
3. 实现代码
public class IndexSyncService {
private final DocumentStore documentStore;
private final VectorStore vectorStore;
private final EmbeddingService embeddingService;
private final MessageQueue messageQueue;
// 增量更新
public void onDocumentChanged(String docId) {
Document doc = documentStore.get(docId);
String newHash = doc.calculateHash();
// 检测变更
if (doc.getLastIndexedHash().equals(newHash)) {
return; // 无变更
}
// 找出变更的 Chunk
List<Chunk> changedChunks = findChangedChunks(doc);
// 更新变更的 Chunk
for (Chunk chunk : changedChunks) {
float[] embedding = embeddingService.embed(chunk.getText());
vectorStore.upsert(
docId + ":" + chunk.getId(),
embedding,
chunk.getMetadata()
);
}
// 更新版本号
doc.setLastIndexedHash(newHash);
documentStore.update(doc);
}
// 异步更新(通过消息队列)
public void asyncSync(String docId) {
messageQueue.publish("doc-changed", docId);
}
// 消费者
@SqsListener("doc-changed")
public void onMessage(String docId) {
onDocumentChanged(docId);
}
}
4. 常见追问
- 追问 1:如何处理删除的文档?
- 向量索引中标记为已删除或物理删除,检索时过滤
- 追问 2:如何保证不丢更新?
- 使用 At-Least-Once 投递 + 幂等消费 + 定期全量校验
一句话总结
通过变更检测、增量更新、消息队列解耦和定期校验的组合机制,实现向量索引与原文的最终一致性。
混合检索时,如果关键词匹配但语义不匹配怎么办?
原始问法:
- 混合检索时,如果关键词匹配但语义不匹配怎么办?
来源题目:
SRC-15-152-471
面试先答
混合检索中关键词匹配但语义不匹配的情况很常见,比如搜索"苹果手机"时,关键词检索到了"苹果公司财务报告"。解决方案是 在融合阶段引入语义相关性过滤和重排序。具体做法:一是 语义过滤,对关键词检索结果计算语义相似度,低于阈值的直接丢弃;二是 重排序(Rerank),用交叉编码器(Cross-Encoder)对融合结果进行精排,同时考虑关键词匹配度和语义相似度;三是 加权降权,对关键词匹配但语义低的结果给予较低权重。核心思想是让语义相关性成为排序的主导因素,而非仅仅关键词匹配。
核心结论
- 关键词匹配但语义不匹配是混合检索的常见问题
- 解决方案:语义过滤 + 重排序 + 加权降权
- 推荐使用 Cross-Encoder 做精排,效果最好
1. 解决方案
方案一:语义过滤
public List<ScoredDoc> semanticFilter(String query,
List<ScoredDoc> keywordResults, double threshold) {
float[] queryEmbedding = embeddingService.embed(query);
return keywordResults.stream()
.filter(doc -> {
float[] docEmbedding = embeddingService.embed(doc.getText());
double similarity = cosineSimilarity(queryEmbedding, docEmbedding);
return similarity > threshold;
})
.collect(Collectors.toList());
}
方案二:Rerank(推荐)
public List<ScoredDoc> rerank(String query, List<ScoredDoc> candidates) {
// Cross-Encoder 同时考虑查询和文档的交互特征
List<RerankedResult> reranked = crossEncoder.rerank(query, candidates);
return reranked.stream()
.sorted(Comparator.comparingDouble(RerankedResult::getScore).reversed())
.map(r -> candidates.get(r.getOriginalIndex()))
.collect(Collectors.toList());
}
方案三:融合降权
public double fusedScore(double keywordScore, double semanticScore,
double semanticWeight) {
// 语义相关性低时,整体分数大幅降低
double semanticFactor = semanticScore > 0.5 ? 1.0 : 0.3;
return keywordScore * semanticFactor * semanticWeight;
}
2. 流程优化
并行检索
├── 关键词检索 → TopK 结果
├── 语义检索 → TopK 结果
│
└── 融合
↓
语义过滤(去除语义不相关的关键词结果)
↓
Rerank(Cross-Encoder 精排)
↓
返回最终结果
3. 常见追问
- 追问 1:Cross-Encoder 为什么效果更好?
- 它同时处理 query 和 document 的交互特征,能捕捉更细粒度的相关性
- 追问 2:Rerank 的性能如何?
- 比向量检索慢,通常只对融合后的 TopN(如 50-100)进行精排
一句话总结
通过语义过滤、Cross-Encoder 重排序和加权降权,让语义相关性成为最终排序的主导因素,有效处理关键词匹配但语义不匹配的问题。
RAG效果优化有哪些方法?
原始问法:
- RAG效果优化有哪些方法?
来源题目:
SRC-15-152-472
面试先答
RAG 效果优化可以从 检索、增强、生成、评估 四个环节入手。检索层面:优化 Chunk 切片策略、改进嵌入模型、使用混合检索、引入 Rerank 精排;增强层面:添加元数据过滤(时间、权限、来源)、利用 HyDE(假设性文档嵌入)、增加查询改写;生成层面:优化 Prompt 模板、控制检索文档数量和顺序、添加引用约束;评估层面:定期评估召回质量、人工标注反馈、持续迭代优化。关键思路是 每个环节都有可优化空间,整体效果提升来自多环节的累加。
核心结论
- 优化四维度:检索、增强、生成、评估
- 关键技术:HyDE、Rerank、混合检索、元数据过滤
- 核心思路:多环节累加优化
1. 检索层优化
| 优化方向 | 具体方法 | 效果 |
|---|---|---|
| Chunk 策略 | 语义切片 + 层级索引 | 提升召回精度 |
| 嵌入模型 | 选更好的嵌入模型(如 BGE-M3) | 提升语义理解 |
| 混合检索 | 关键词 + 语义融合 | 兼顾精确和语义 |
| Rerank | Cross-Encoder 精排 | 提升排序质量 |
| 索引优化 | HNSW/IVF 加速检索 | 提升检索速度 |
2. 增强层优化
HyDE(假设性文档嵌入)
// 让 LLM 先生成一个"假设性答案",再用假设答案做检索
public String hydeSearch(String query) {
// Step 1: 生成假设性答案
String hypothetical = llm.generate(
"根据以下问题生成一个可能的答案:" + query
);
// Step 2: 用假设性答案做检索(通常比原问题效果好)
return vectorSearch(hypothetical);
}
查询改写
public String rewriteQuery(String query) {
return llm.generate(
"将以下查询改写为更适合检索的形式," +
"包含关键词扩展和语义澄清:" + query
);
}
元数据过滤
public List<Document> searchWithFilter(String query,
String source, LocalDate fromDate) {
return vectorStore.search(
query,
Filter.builder()
.eq("source", source)
.gte("publish_date", fromDate)
.build()
);
}
3. 生成层优化
Prompt 优化
System Prompt:
你是一个基于检索内容回答问题的助手。
规则:
1. 只基于提供的上下文回答,不要编造
2. 在答案中标注引用来源 [1][2]
3. 如果上下文不足以回答,明确说明
4. 优先使用最新的信息
Context:
[1] {doc1_content}
[2] {doc2_content}
Question: {user_query}
文档选择与排序
public List<Document> selectDocuments(List<Document> docs, int maxDocs) {
// 按相关性排序
docs.sort(Comparator.comparingDouble(Document::getScore).reversed());
// 去重 + 多样性保证
List<Document> selected = new ArrayList<>();
Set<String> seenSources = new HashSet<>();
for (Document doc : docs) {
if (selected.size() >= maxDocs) break;
if (!seenSources.contains(doc.getSource())) {
selected.add(doc);
seenSources.add(doc.getSource());
}
}
return selected;
}
4. 常见追问
- 追问 1:哪个环节优化收益最大?
- 通常检索层(嵌入模型 + 混合检索)收益最大
- 追问 2:如何平衡优化效果和成本?
- 先用简单方法(混合检索+Rerank),效果不足再引入更复杂的方法
一句话总结
RAG 效果优化从检索、增强、生成、评估四维度入手,通过 HyDE、混合检索、Rerank、元数据过滤等技术多环节累加提升。
什么是CoT(思维链)?
原始问法:
- 什么是CoT(思维链)?
来源题目:
SRC-15-152-473
面试先答
CoT(Chain of Thought,思维链)是一种 引导大模型进行分步推理的 Prompt 技术,通过要求模型"一步步思考"来提升复杂推理任务的准确性。核心思想是让模型在生成最终答案前,先生成中间推理步骤,模拟人类解决问题的思维过程。CoT 在数学推理、逻辑推理、多步骤问答等场景效果显著,能将准确率提升 10-40%。实现方式有两种:Zero-shot CoT(在 Prompt 中加入"让我们一步步思考")和 Few-shot CoT(在 Prompt 中提供带推理步骤的示例)。
核心结论
- CoT 是引导大模型分步推理的 Prompt 技术
- 核心价值:提升复杂推理任务的准确率
- 两种实现:Zero-shot CoT 和 Few-shot CoT
1. 是什么
CoT(Chain of Thought)是一种 Prompt Engineering 技术,通过要求大模型在回答问题时 生成中间推理步骤(思维链),而非直接给出答案,从而提升推理准确性。
没有 CoT:
用户:一个商店有 23 个苹果,卖了 17 个,又进货 8 个,还剩多少?
模型:34 个(可能算错)
有 CoT:
用户:一个商店有 23 个苹果,卖了 17 个,又进货 8 个,还剩多少?
模型:让我们一步步思考:
1. 初始 23 个
2. 卖了 17 个:23 - 17 = 6 个
3. 进货 8 个:6 + 8 = 14 个
4. 答案是 14 个
(准确率显著提升)
2. 实现方式
Zero-shot CoT
Prompt:
问题:{question}
让我们一步步思考,然后给出答案。
Few-shot CoT
Prompt:
示例:
问题:小明有 5 个糖果,给了小红 3 个,又买了 2 个
思考过程:
1. 初始 5 个
2. 给了 3 个:5 - 3 = 2
3. 买了 2 个:2 + 2 = 4
答案:4
问题:{question}
思考过程:
Self-Consistency CoT
// 多次采样,取多数答案
public String selfConsistencyCoT(String question, int samples) {
Map<String, Integer> answerCounts = new HashMap<>();
for (int i = 0; i < samples; i++) {
String response = llm.coTGenerate(question);
String answer = extractAnswer(response);
answerCounts.merge(answer, 1, Integer::sum);
}
return answerCounts.entrySet().stream()
.max(Map.Entry.comparingByValue())
.getKey();
}
3. 适用场景
- 数学推理(计算、应用题)
- 逻辑推理(智力题、判断题)
- 多步骤问答(需综合多条信息)
- 代码生成(分步实现)
4. 局限性
- 简单问题不需要 CoT(增加延迟和 Token 消耗)
- 推理链条过长可能出错
- 模型可能生成看似合理但错误的推理步骤
5. 常见追问
- 追问 1:CoT 和 RAG 的关系?
- CoT 是推理增强,RAG 是知识增强,可结合使用
- 追问 2:如何让 CoT 更有效?
- 提供高质量示例、控制推理链条长度、使用 Self-Consistency
一句话总结
CoT 通过引导大模型生成分步推理过程,有效提升复杂推理任务的准确性,是 Prompt Engineering 中的核心技术。
RAG如何解决大模型幻觉?怎么体现/评估?
原始问法:
- RAG如何解决大模型幻觉?怎么体现/评估?
来源题目:
SRC-15-152-474
面试先答
RAG 通过 "检索增强" 从根本上减少大模型幻觉。大模型幻觉的根源是训练数据中的知识过时或缺失,模型只能"编造"答案。RAG 的解决思路是:不让模型依赖内部知识,而是通过检索提供最新、可靠的外部知识作为上下文,让模型"基于证据回答"而非"凭记忆回答"。具体机制包括:在 Prompt 中注入检索到的权威文档、要求模型标注引用来源、约束模型只基于提供内容回答。评估方面通过 忠实度评估(答案是否完全基于检索内容)和 引用准确率(引用来源是否正确相关)两个核心指标衡量效果。
核心结论
- RAG 通过提供外部权威知识减少幻觉
- 机制:检索增强 + 引用约束 + Prompt 指令
- 评估:忠实度 + 引用准确率
1. 幻觉类型与 RAG 解决方案
| 幻觉类型 | 表现 | RAG 解决方案 |
|---|---|---|
| 事实幻觉 | 编造不存在的事实/数据 | 提供权威文档作为知识来源 |
| 引用幻觉 | 编造引用来源 | 要求模型引用真实的检索文档 |
| 逻辑幻觉 | 推理逻辑错误 | 提供正确的推理上下文 |
| 过度泛化 | 超出知识范围回答 | 约束模型"只基于提供内容回答" |
2. RAG 防幻觉机制
用户提问
↓
检索权威文档(知识注入)
↓
构建 Prompt:
System: 只基于提供的上下文回答,不编造信息。
Context: [检索到的文档内容]
Question: 用户问题
Instruction: 如果上下文不足,说明"根据现有信息无法回答"
↓
模型生成答案(基于检索内容)
↓
验证答案引用(确保引用来源真实)
3. 评估方法
忠实度评估
public class FaithfulnessEvaluator {
public FaithfulnessResult evaluate(String answer,
List<Document> retrievedDocs) {
// 将答案切分为原子陈述
List<String> claims = extractClaims(answer);
int supportedClaims = 0;
List<String> unsupportedClaims = new ArrayList<>();
for (String claim : claims) {
// 检查每个陈述是否被检索文档支持
boolean supported = checkSupport(claim, retrievedDocs);
if (supported) {
supportedClaims++;
} else {
unsupportedClaims.add(claim);
}
}
double faithfulness = (double) supportedClaims / claims.size();
return FaithfulnessResult.builder()
.faithfulness(faithfulness)
.totalClaims(claims.size())
.supportedClaims(supportedClaims)
.unsupportedClaims(unsupportedClaims)
.build();
}
}
引用准确率评估
public class CitationEvaluator {
public CitationResult evaluate(String answer,
List<Document> retrievedDocs) {
// 提取答案中的引用
List<Citation> citations = extractCitations(answer);
int correctCitations = 0;
List<Citation> incorrectCitations = new ArrayList<>();
for (Citation citation : citations) {
// 检查引用是否对应正确的文档
if (citation.isValid(retrievedDocs)) {
correctCitations++;
} else {
incorrectCitations.add(citation);
}
}
double accuracy = citations.isEmpty() ? 1.0
: (double) correctCitations / citations.size();
return CitationResult.builder()
.citationAccuracy(accuracy)
.totalCitations(citations.size())
.correctCitations(correctCitations)
.build();
}
}
4. 进一步减少幻觉的技巧
- Prompt 约束:明确要求"只基于提供的上下文回答"
- 拒答机制:当检索内容不足时,允许模型回答"无法回答"
- 引用约束:要求答案中的每个论点都有引用支持
- 温度设置:降低 temperature 减少创造性但错误的输出
- 多轮验证:生成后用 LLM 交叉验证答案正确性
5. 常见追问
- 追问 1:RAG 能完全消除幻觉吗?
- 不能。如果检索本身召回了错误文档,模型仍会基于错误文档生成"正确回答"
- 追问 2:检索质量差时怎么办?
- 优化检索策略(混合检索、Rerank)、增加拒答概率、引入人工审核
一句话总结
RAG 通过检索注入外部权威知识,让模型"基于证据回答"而非"凭记忆回答",通过忠实度和引用准确率评估效果,大幅减少但无法完全消除幻觉。
常用哪些大模型?模型选型的考虑因素有哪些?
原始问法:
- 常用哪些大模型?模型选型的考虑因素有哪些?
来源题目:
SRC-15-153-475
面试先答
当前主流大模型可分为 三大阵营:闭源商用模型(GPT-4o/4、Claude 3.5/3、Gemini Pro/Ultra)、开源模型(Llama 3/3.1、Mistral、Qwen 系列)、国内模型(文心一言、通义、千问、混元)。选型需要综合考虑 六大因素:一是能力需求(推理、代码、多模态等);二是成本预算(Token 价格、调用费用);三是数据安全(是否允许数据外传、是否需要私有化部署);四是响应延迟(实时性要求);五是上下文窗口大小;六是生态工具链(是否支持 Function Call、RAG 等)。没有最好的模型,只有最适合场景的模型。
核心结论
- 三大阵营:闭源商用、开源、国内模型
- 选型六因素:能力、成本、安全、延迟、上下文、生态
- 核心原则:场景匹配优先,不盲目追求最新最强
1. 主流模型概览
| 类别 | 代表模型 | 核心优势 |
|---|---|---|
| 闭源高端 | GPT-4o/4、Claude 3.5 Opus | 推理能力强、支持多模态 |
| 闭源性价比 | GPT-4o-mini、Claude 3 Haiku | 速度快、成本低 |
| 开源旗舰 | Llama 3.1 70B、Qwen 2.5 72B | 可私有化、社区活跃 |
| 开源轻量 | Phi-3、Gemma 2、Mistral 7B | 本地化部署、低资源需求 |
| 国内模型 | 千问 Max、文心一言 4.0、混元 | 中文优化、合规支持 |
2. 选型决策流程
需求分析
↓
┌─────────────────────┐
│ 1. 能力需求评估 │
│ - 推理/代码/多模态?│
│ - 中文/英文? │
│ - 准确率要求? │
└─────────┬───────────┘
↓
┌─────────────────────┐
│ 2. 安全与合规 │
│ - 数据可外传? │
│ - 需要私有化? │
│ - 行业合规要求? │
└─────────┬───────────┘
↓
┌─────────────────────┐
│ 3. 成本与性能 │
│ - 预算范围? │
│ - 延迟要求? │
│ - 吞吐量? │
└─────────┬───────────┘
↓
模型选型决策
3. 常见场景推荐
| 场景 | 推荐模型 | 理由 |
|---|---|---|
| 企业级问答 | GPT-4o / Claude 3.5 Sonnet | 能力全面、RAG 支持好 |
| 代码辅助 | GPT-4o / Claude 3.5 Opus | 代码生成能力强 |
| 本地化部署 | Llama 3.1 / Qwen 2.5 | 开源可私有化 |
| 低成本场景 | GPT-4o-mini / Haiku | 性价比高 |
| 国内合规 | 千问 / 文心一言 | 数据不出境、中文优化 |
| 边缘设备 | Phi-3 / Gemma 2 | 轻量级、可本地运行 |
4. 常见追问
- 追问 1:闭源 vs 开源如何选择?
- 数据敏感→开源私有化;追求效果→闭源;预算有限→开源
- 追问 2:模型多久换一次?
- 根据业务需求和模型迭代节奏,通常半年评估一次
一句话总结
大模型选型需综合能力、成本、安全、延迟、上下文和生态六大因素,按场景匹配而非盲目追求最强。
大模型API调用原理是什么?流式输出怎么实现?
原始问法:
- 大模型API调用原理是什么?流式输出怎么实现?
来源题目:
SRC-15-153-476
面试先答
大模型 API 调用本质是 HTTP 请求-响应 的封装,客户端发送包含 Prompt 和参数的 JSON 请求到 API 端点,服务端执行推理后返回生成结果。调用流程包括:客户端组装请求(消息、模型名、参数)→ HTTP 请求发送 → 服务端排队与推理 → 结果返回。流式输出 通过 Server-Sent Events(SSE) 实现:服务端按 Token 粒度逐步生成,每生成一个 Token 就通过 SSE 推送给客户端,客户端实时接收并渲染。核心好处是用户无需等待全部生成完成即可看到输出,大幅改善体验。Java 中可用 OkHttp 或 Spring WebFlux 的 Flux 处理 SSE 流。
核心结论
- API 调用 = HTTP 请求-响应的封装
- 流式输出 = SSE(Server-Sent Events)按 Token 粒度逐步推送
- 核心价值:提升用户体验,支持实时渲染
1. API 调用流程
┌──────────┐ HTTP POST ┌──────────────┐
│ 客户端 │─────────────────→│ API 网关 │
│ │ │ (鉴权/路由) │
└────┬─────┘ └──────┬───────┘
│ │
│ ┌──────────▼──────────┐
│ │ 模型服务 │
│ │ (推理引擎 + GPU) │
│ └──────────┬──────────┘
│ │
│ 推理完成,返回结果
│ │
│←────────── HTTP Response ────┘
│
▼
客户端处理结果
请求结构
{
"model": "gpt-4o",
"messages": [
{"role": "system", "content": "你是一个助手"},
{"role": "user", "content": "你好"}
],
"temperature": 0.7,
"max_tokens": 1000,
"stream": false
}
2. 流式输出实现
SSE 协议
服务端按 Token 粒度推送:
data: {"choices":[{"delta":{"content":"你"}}]}
data: {"choices":[{"delta":{"content":"好"}}]}
data: {"choices":[{"delta":{"content":"!"}}]}
data: [DONE]
Java 实现
// 使用 OkHttp 调用流式 API
OkHttpClient client = new OkHttpClient();
RequestBody body = RequestBody.create(
jsonPayload,
MediaType.parse("application/json")
);
Request request = new Request.Builder()
.url("https://api.openai.com/v1/chat/completions")
.header("Authorization", "Bearer " + apiKey)
.post(body)
.build();
// 流式处理
try (Response response = client.newCall(request).execute()) {
BufferedSource source = response.body().source();
while (!source.exhausted()) {
String line = source.readUtf8Line();
if (line.startsWith("data: ")) {
String data = line.substring(6);
if ("[DONE]".equals(data)) break;
// 解析并处理每个 Token
processToken(data);
}
}
}
Spring WebFlux 实现
// 使用 WebClient 处理流式响应
WebClient client = WebClient.create();
Flux<String> tokenStream = client.post()
.uri("/v1/chat/completions")
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(requestBody)
.retrieve()
.bodyToFlux(String.class);
tokenStream.subscribe(
token -> {
// 实时渲染 Token
appendToUI(token);
},
error -> log.error("Stream error", error),
() -> log.info("Stream completed")
);
3. 常见追问
- 追问 1:SSE 和 WebSocket 的区别?
- SSE 是单向(服务端→客户端),WebSocket 是双向;SSE 基于 HTTP 更简单
- 追问 2:流式输出如何处理中断?
- 客户端断连后服务端停止生成;可通过断点续传恢复
一句话总结
大模型 API 基于 HTTP 请求-响应,流式输出通过 SSE 协议按 Token 粒度逐步推送给客户端,实现实时响应体验。
写提示词的方法论是什么?对提示词工程的理解?
原始问法:
- 写提示词的方法论是什么?对提示词工程的理解?
来源题目:
SRC-15-153-477
面试先答
提示词工程(Prompt Engineering)是一门 与大模型有效沟通的艺术和科学,核心目标是通过精心设计的输入引导模型生成高质量输出。方法论可以总结为 CREATE 原则:Clear(清晰明确的指令)、Role(设定专业角色)、Example(提供示例)、Aspect(指定输出维度)、Tone(调整语气风格)、Elaborate(细化要求)。关键技巧包括:使用 System Prompt 设定行为边界、Few-shot 提供示例引导、结构化输出要求、分步推理(CoT)、迭代优化。本质上是通过优化输入信号来最大化模型输出质量。
核心结论
- 提示词工程 = 与大模型沟通的方法论,核心是 CREATE 原则
- 关键技巧:角色设定、Few-shot、结构化输出、CoT、迭代优化
- 本质:优化输入信号,最大化输出质量
1. CREATE 原则详解
| 原则 | 说明 | 示例 |
|---|---|---|
| Clear | 清晰明确的指令 | "将以下中文翻译为英文,保持原意" |
| Role | 设定专业角色 | "你是一名资深 Java 架构师" |
| Example | 提供 Few-shot 示例 | "示例:输入:1+1 输出:2。现在计算:3+5" |
| Aspect | 指定输出维度 | "从性能、安全性、可维护性三个方面分析" |
| Tone | 调整语气风格 | "用通俗易懂的口语化风格回答" |
| Elaborate | 细化要求 | "代码需添加注释,变量使用驼峰命名" |
2. 提示词结构模板
# System Prompt(设定身份和行为规则)
你是一名{role},擅长{skill}。
规则:
1. {rule1}
2. {rule2}
# Context(提供背景信息)
背景:{context}
# Instruction(明确任务指令)
请{task_description}
# Example(提供示例,可选)
示例:
输入:{example_input}
输出:{example_output}
# Output Format(指定输出格式)
输出格式:{format_spec}
# User Input(用户输入)
{user_input}
3. 高级技巧
Few-shot 示例
请将以下技术术语解释为通俗易懂的比喻:
示例:
术语:JVM
解释:JVM 就像一个"翻译官",把 Java 代码翻译成电脑能听懂的指令
术语:HashMap
解释:
结构化输出约束
请以 JSON 格式输出,包含以下字段:
{
"answer": "详细答案",
"confidence": 0.0-1.0的置信度,
"sources": ["引用来源列表"]
}
分步引导(CoT)
请一步步思考以下问题,然后给出最终答案:
问题:{question}
思考过程:
4. 迭代优化流程
1. 写初始 Prompt
↓
2. 测试典型用例
↓
3. 分析输出问题
├── 指令不清晰 → 简化指令
├── 输出不符合格式 → 添加格式约束
├── 知识不足 → 添加背景信息
└── 风格不对 → 调整角色/语气
↓
4. 优化 Prompt
↓
5. 重新测试
↓
6. 迭代至满意
5. 常见追问
- 追问 1:提示词工程和传统编程的区别?
- 编程是确定性的逻辑,提示词工程是概率性的引导
- 追问 2:如何评估 Prompt 质量?
- 通过准确率、完整性、相关性等指标,批量测试用例评估
一句话总结
提示词工程通过 CREATE 六大原则和角色设定、Few-shot、结构化输出等技巧,系统化地引导大模型生成高质量输出,是 AI 应用开发的核心技能。
上下文工程的理解?和提示词工程的区别?
原始问法:
- 上下文工程的理解?和提示词工程的区别?
来源题目:
SRC-15-153-478
面试先答
上下文工程(Context Engineering) 是比提示词工程更宽泛的概念,指 管理和优化大模型接收的所有信息 的工程实践。提示词工程关注的是 Prompt 本身的编写技巧,是上下文工程的一个子集。上下文工程涵盖的范围更广,包括:提示词设计(Prompt Engineering)、历史对话管理(选择/压缩/过滤历史)、外部知识注入(RAG 检索结果、知识库查询)、工具调用上下文(工具定义和执行结果)、多模态信息处理(图片、文件等)。简单说:提示词工程管"写什么话",上下文工程管"给模型看什么信息"。上下文工程是 Agent 系统设计的核心能力。
核心结论
- 上下文工程 > 提示词工程,前者是后者的超集
- 上下文工程关注给模型看什么信息,提示词工程关注写什么 Prompt
- 上下文工程是 Agent 系统设计的核心,涉及多维度信息管理
1. 对比分析
| 维度 | 提示词工程 | 上下文工程 |
|---|---|---|
| 关注点 | Prompt 编写技巧 | 所有输入信息的管理 |
| 范围 | 窄(Prompt 本身) | 宽(全部信息输入) |
| 内容 | 指令、角色、示例 | Prompt + 历史 + 知识 + 工具 + 多模态 |
| 目标 | 提升输出质量 | 优化信息输入质量 |
| 关系 | 上下文工程的子集 | 包含提示词工程 |
2. 上下文工程的组成
┌─────────────────────────────────────────────────┐
│ 上下文工程 │
├─────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ │
│ │ 1. 提示词设计 │ ← 提示词工程 │
│ └──────────────┘ │
│ │
│ ┌──────────────┐ │
│ │ 2. 历史管理 │ 选择/压缩/隔离历史对话 │
│ └──────────────┘ │
│ │
│ ┌──────────────┐ │
│ │ 3. 知识注入 │ RAG 检索结果、知识库 │
│ └──────────────┘ │
│ │
│ ┌──────────────┐ │
│ │ 4. 工具上下文 │ 工具定义、执行结果 │
│ └──────────────┘ │
│ │
│ ┌──────────────┐ │
│ │ 5. 多模态处理 │ 图片、文件、代码等 │
│ └──────────────┘ │
│ │
└─────────────────────────────────────────────────┘
3. 历史对话管理策略
public class ContextManager {
// 策略一:滑动窗口(保留最近 N 轮)
public List<Message> slidingWindow(List<Message> history, int windowSize) {
int start = Math.max(0, history.size() - windowSize);
return history.subList(start, history.size());
}
// 策略二:摘要压缩(旧对话压缩为摘要)
public List<Message> summarizeAndCompress(List<Message> history) {
// 旧对话 → LLM 生成摘要 → 摘要作为上下文
String summary = llm.summarize(history.subList(0, history.size() - 5));
List<Message> result = new ArrayList<>();
result.add(new SystemMessage("对话摘要:" + summary));
result.addAll(history.subList(history.size() - 5, history.size()));
return result;
}
// 策略三:关键信息提取(只保留关键信息)
public List<Message> extractKeyInfo(List<Message> history) {
// 提取用户偏好、决策、关键数据
return history.stream()
.filter(msg -> msg.isKeyInformation())
.collect(Collectors.toList());
}
}
4. 常见追问
- 追问 1:上下文工程的核心挑战?
- Token 窗口限制、信息选择质量、实时性保证
- 追问 2:如何评估上下文质量?
- 通过任务完成率、答案准确率、Token 利用率等指标间接评估
一句话总结
上下文工程管理大模型接收的所有信息,是比提示词工程更宽泛的概念,涵盖 Prompt 设计、历史管理、知识注入、工具上下文和多模态处理。
如何避免不同用户上下文互相污染?
原始问法:
- 如何避免不同用户上下文互相污染?
来源题目:
SRC-15-153-479
面试先答
避免不同用户上下文污染的核心是 建立严格的上下文隔离机制,主要有四种方案:会话级隔离(每个用户会话独立存储上下文)、用户级隔离(按用户 ID 隔离所有历史和状态)、租户级隔离(多租户场景下按租户隔离)、请求级隔离(每次请求独立处理,不依赖历史)。实现上通过 Session ID / User ID 作为隔离键、上下文存储按 ID 分区、上下文数据结构设计时就考虑隔离来保证。同时需要注意 Token 化的上下文标识传递,确保请求路由到正确的上下文空间。
核心结论
- 隔离方案:会话级、用户级、租户级、请求级
- 核心机制:ID 隔离 + 存储分区 + 标识传递
- 设计原则:默认隔离,按需共享
1. 隔离策略
| 隔离级别 | 隔离键 | 适用场景 | 实现方式 |
|---|---|---|---|
| 会话级 | Session ID | 单用户单次对话 | 每个 Session 独立存储 |
| 用户级 | User ID | 单用户多会话 | 按用户聚合历史 |
| 租户级 | Tenant ID | SaaS 多租户 | 按租户完全隔离 |
| 请求级 | Request ID | 无状态请求 | 不存储历史 |
2. 实现方案
Session 隔离
public class IsolatedContextStore {
private final Map<String, List<Message>> sessionContexts = new ConcurrentHashMap<>();
// 获取用户会话上下文
public List<Message> getContext(String sessionId) {
return sessionContexts.getOrDefault(sessionId, new ArrayList<>());
}
// 更新上下文
public void addMessage(String sessionId, Message message) {
sessionContexts.computeIfAbsent(sessionId, k -> new ArrayList<>())
.add(message);
}
// 清理过期会话
@Scheduled(fixedRate = 3600000) // 每小时
public void cleanExpiredSessions() {
// 清理超过 24 小时未活跃的会话
sessionContexts.entrySet().removeIf(
entry -> isExpired(entry.getKey())
);
}
}
Token 传递
// 请求中携带 Session ID
@RestController
public class AgentController {
@PostMapping("/chat")
public ChatResponse chat(
@RequestBody ChatRequest request,
@RequestHeader("X-Session-Id") String sessionId) {
// 使用 Session ID 隔离上下文
List<Message> context = contextStore.getContext(sessionId);
context.add(new UserMessage(request.getMessage()));
// 调用 Agent
AgentResponse response = agent.run(context);
// 保存上下文
context.add(new AssistantMessage(response.getText()));
contextStore.addMessage(sessionId, new AssistantMessage(response.getText()));
return ChatResponse.from(response);
}
}
Redis 存储隔离
// 使用 Redis 的 Key 前缀隔离
public class RedisContextStore {
private static final String KEY_PREFIX = "agent:context:";
public List<Message> getContext(String sessionId) {
String key = KEY_PREFIX + sessionId;
String json = redisTemplate.opsForValue().get(key);
return json != null ? deserialize(json) : new ArrayList<>();
}
public void saveContext(String sessionId, List<Message> messages) {
String key = KEY_PREFIX + sessionId;
redisTemplate.opsForValue().set(key, serialize(messages), 24, TimeUnit.HOURS);
}
}
3. 常见追问
- 追问 1:用户切换设备如何保持上下文?
- 使用 User ID 做全局标识,Session ID 做设备级标识
- 追问 2:上下文数据安全如何保障?
- 传输加密(HTTPS)、存储加密、访问控制
一句话总结
通过 Session ID / User ID 作为隔离键,在存储层、数据结构层和传输层建立多级隔离机制,确保不同用户上下文互不干扰。
保存/隔离/选择/压缩上下文的方法有哪些?
原始问法:
- 保存/隔离/选择/压缩上下文的方法有哪些?
来源题目:
SRC-15-153-480
面试先答
上下文的 保存、隔离、选择、压缩 是一个完整的管理体系。保存 指将上下文存储在持久化介质(Redis、数据库、文件)中,支持会话恢复;隔离 指按 Session/User/Tenant ID 分区存储,防止不同上下文污染;选择 指从全量历史中挑选与当前任务最相关的上下文(滑动窗口、关键信息提取、语义检索);压缩 指将旧对话压缩为摘要或结构化信息,节省 Token 消耗。实际项目中通常组合使用:持久化保存 → ID 隔离 → 相关性选择 → 摘要压缩,形成完整的上下文生命周期管理。
核心结论
- 四要素:保存(持久化)、隔离(防污染)、选择(提相关)、压缩(省 Token)
- 生命周期:创建→保存→隔离→选择→压缩→清理
- 核心挑战:平衡信息量和 Token 限制
1. 上下文保存
| 存储方式 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| Redis | 会话级缓存 | 高性能、自动过期 | 不适合长期存储 |
| 关系型数据库 | 用户级历史 | 持久化、可查询 | 性能较低 |
| 向量数据库 | 语义检索 | 支持相关性查找 | 成本较高 |
| 文件存储 | 归档历史 | 低成本、大容量 | 查询不便 |
2. 上下文选择策略
public class ContextSelector {
// 策略一:滑动窗口
public List<Message> selectByWindow(List<Message> all, int lastN) {
return all.subList(Math.max(0, all.size() - lastN), all.size());
}
// 策略二:关键词过滤
public List<Message> selectByKeyword(List<Message> all, String keyword) {
return all.stream()
.filter(msg -> msg.getContent().contains(keyword))
.collect(Collectors.toList());
}
// 策略三:语义检索
public List<Message> selectBySemantic(List<Message> all, String query, int topK) {
// 将历史消息向量化,按语义相似度选择
return semanticSearch(all, query, topK);
}
// 策略四:时间窗口
public List<Message> selectByTimeRange(List<Message> all,
LocalDateTime from, LocalDateTime to) {
return all.stream()
.filter(msg -> msg.getTimestamp().isAfter(from)
&& msg.getTimestamp().isBefore(to))
.collect(Collectors.toList());
}
}
3. 上下文压缩策略
public class ContextCompressor {
// 策略一:LLM 摘要压缩
public CompressedContext summarize(List<Message> oldMessages) {
String summary = llm.generate(
"请总结以下对话的关键信息:\n" +
formatMessages(oldMessages)
);
return new CompressedContext(summary, oldMessages.size());
}
// 策略二:结构化提取
public StructuredContext extractStructure(List<Message> messages) {
Map<String, Object> structure = new HashMap<>();
structure.put("user_preferences", extractPreferences(messages));
structure.put("key_decisions", extractDecisions(messages));
structure.put("entities", extractEntities(messages));
return new StructuredContext(structure);
}
// 策略三:Token 预算分配
public List<Message> allocateBudget(List<Message> messages, int maxTokens) {
// 系统提示 + 摘要 + 最近对话
List<Message> result = new ArrayList<>();
result.add(getSystemPrompt());
result.add(getSummary(messages));
result.addAll(getRecentMessagesWithinBudget(messages, maxTokens));
return result;
}
}
4. 常见追问
- 追问 1:压缩后信息丢失怎么办?
- 采用分层压缩:保留详细摘要 + 关键原文片段
- 追问 2:如何设计压缩策略?
- 动态调整:上下文充足时少压缩,紧张时多压缩
一句话总结
通过保存、隔离、选择、压缩四要素的组合策略,实现上下文的完整生命周期管理,在信息量和 Token 限制之间取得平衡。
如何持久化上下文?
原始问法:
- 如何持久化上下文?
来源题目:
SRC-15-153-481
面试先答
上下文持久化的核心是 将对话历史和状态存储到可持久化介质中,支持会话恢复和跨设备同步。主要方案包括:Redis + 数据库组合(Redis 存热数据、数据库存冷数据)、纯数据库方案(简单场景直接用 MySQL/PostgreSQL)、向量数据库 + 关系数据库(支持语义检索的持久化)。持久化的关键设计包括:存储结构设计(消息表 + 会话表 + 元数据表)、自动过期机制(TTL + 定期清理)、读写性能优化(批量写入、异步持久化)。核心挑战是平衡性能和可靠性,通常采用"内存缓存 + 异步持久化"的组合模式。
核心结论
- 持久化方案:Redis + 数据库、纯数据库、向量库 + 关系库
- 关键设计:存储结构、自动过期、性能优化
- 推荐模式:内存缓存 + 异步持久化
1. 存储结构设计
-- 会话表
CREATE TABLE conversation (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
session_id VARCHAR(64) NOT NULL UNIQUE,
user_id VARCHAR(64) NOT NULL,
title VARCHAR(255),
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_user_id (user_id)
);
-- 消息表
CREATE TABLE message (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
conversation_id BIGINT NOT NULL,
role ENUM('system', 'user', 'assistant', 'tool') NOT NULL,
content TEXT NOT NULL,
metadata JSON,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_conversation_id (conversation_id)
);
-- 上下文元数据表
CREATE TABLE context_metadata (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
session_id VARCHAR(64) NOT NULL,
context_summary TEXT,
key_facts JSON,
entities JSON,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uk_session_id (session_id)
);
2. 持久化实现
@Service
public class ContextPersistenceService {
private final ConversationRepository conversationRepo;
private final MessageRepository messageRepo;
private final RedisTemplate<String, List<Message>> redisTemplate;
// 异步持久化(写入消息后立即返回)
@Async
public void persistMessage(String sessionId, Message message) {
// 1. 更新 Redis 缓存(热数据)
String redisKey = "context:" + sessionId;
List<Message> cached = redisTemplate.opsForList().range(redisKey, 0, -1);
cached.add(message);
redisTemplate.opsForList().rightPush(redisKey, message);
// 2. 异步写入数据库(冷数据)
Conversation conversation = conversationRepo
.findBySessionId(sessionId)
.orElseGet(() -> createConversation(sessionId));
MessageEntity entity = MessageEntity.from(message, conversation.getId());
messageRepo.save(entity);
}
// 启动时恢复上下文
public List<Message> restoreContext(String sessionId) {
// 优先从 Redis 读取
List<Message> cached = redisTemplate.opsForList()
.range("context:" + sessionId, 0, -1);
if (cached != null && !cached.isEmpty()) {
return cached;
}
// Redis 没有,从数据库恢复
return messageRepo.findBySessionIdOrderByCreatedAtAsc(sessionId)
.stream()
.map(MessageEntity::toMessage)
.collect(Collectors.toList());
}
}
3. 自动清理策略
@Scheduled(fixedRate = 86400000) // 每天执行
public void cleanExpiredContexts() {
// 清理超过 30 天的会话
LocalDateTime threshold = LocalDateTime.now().minusDays(30);
List<Conversation> expired = conversationRepo
.findByUpdatedAtBefore(threshold);
for (Conversation conv : expired) {
// 删除消息和会话
messageRepo.deleteByConversationId(conv.getId());
conversationRepo.delete(conv);
// 清理 Redis 缓存
redisTemplate.delete("context:" + conv.getSessionId());
}
}
4. 常见追问
- 追问 1:如何保证持久化不丢数据?
- 同步写数据库 + 异步优化,关键消息同步写入
- 追问 2:大量历史数据如何处理?
- 冷热分离:热数据存 Redis,冷数据归档或清理
一句话总结
通过 Redis 缓存 + 数据库持久化的组合模式,配合合理的存储结构和清理策略,实现上下文的高效可靠持久化。
多轮prompt中当前prompt怎么利用之前的prompt?
原始问法:
- 多轮prompt中当前prompt怎么利用之前的prompt?
来源题目:
SRC-15-153-482
面试先答
多轮对话中当前 Prompt 利用之前 Prompt 的核心是 上下文传递机制,主要通过 三种方式:完整历史拼接(将所有历史消息按顺序拼接在当前 Prompt 前面,模型自行理解上下文)、摘要+关键信息保留(旧对话压缩为摘要,保留最近几轮完整对话)、结构化上下文注入(将历史中的关键信息提取为结构化数据注入当前 Prompt)。实现上通过维护消息列表,每次调用时将历史消息和当前用户消息一起发送给模型。关键设计是控制上下文长度,在信息完整性和 Token 限制之间取得平衡。
核心结论
- 多轮 Prompt 利用历史的三种方式:完整拼接、摘要保留、结构化注入
- 核心机制:消息列表维护 + 上下文长度控制
- 关键挑战:平衡信息完整性和 Token 限制
1. 三种利用方式
方式一:完整历史拼接
// 直接将所有历史消息拼接
List<Message> messages = new ArrayList<>();
messages.add(new SystemMessage("你是一个助手"));
messages.addAll(conversationHistory); // 所有历史
messages.add(new UserMessage(currentInput));
ChatResponse response = llm.chat(messages);
方式二:摘要 + 最近对话
// 旧对话压缩为摘要 + 最近 N 轮完整保留
List<Message> buildContext(List<Message> history,
Message current, int keepLastN) {
List<Message> context = new ArrayList<>();
// 系统提示
context.add(new SystemMessage("你是一个助手"));
// 历史摘要
if (history.size() > keepLastN) {
String summary = summarizeHistory(
history.subList(0, history.size() - keepLastN)
);
context.add(new SystemMessage("历史摘要:" + summary));
}
// 最近对话
int start = Math.max(0, history.size() - keepLastN);
context.addAll(history.subList(start, history.size()));
// 当前消息
context.add(current);
return context;
}
方式三:结构化上下文注入
// 从历史中提取关键信息,结构化注入
Map<String, Object> extractedContext = extractKeyInfo(history);
String enhancedPrompt = """
基于以下上下文信息回答问题:
用户偏好:%s
之前的决策:%s
关键数据:%s
当前问题:%s
""".formatted(
extractedContext.get("preferences"),
extractedContext.get("decisions"),
extractedContext.get("key_data"),
currentInput
);
2. 上下文窗口管理
public class WindowManager {
private static final int MAX_TOKENS = 4000;
private static final int SUMMARY_THRESHOLD = 3000;
public List<Message> manageWindow(List<Message> messages) {
int currentTokens = countTokens(messages);
if (currentTokens <= MAX_TOKENS) {
return messages; // 直接返回
}
// 超过阈值,触发压缩
List<Message> result = new ArrayList<>();
result.add(messages.get(0)); // 保留 System Prompt
// 计算需要压缩多少
int keepTokens = MAX_TOKENS - countTokens(
messages.get(0),
messages.get(messages.size() - 1)
);
// 保留最近的消息,压缩中间的
List<Message> toCompress = new ArrayList<>();
List<Message> toKeep = new ArrayList<>();
int midPoint = findSplitPoint(messages, keepTokens);
toCompress.addAll(messages.subList(1, midPoint));
toKeep.addAll(messages.subList(midPoint, messages.size()));
// 压缩 + 拼接
result.add(new SystemMessage("历史摘要:" + summarize(toCompress)));
result.addAll(toKeep);
return result;
}
}
3. 常见追问
- 追问 1:如何避免历史信息丢失?
- 摘要时保留关键信息点,重要场景不压缩完整保留
- 追问 2:System Prompt 在多轮中如何处理?
- 通常固定在最前面,不参与压缩
一句话总结
多轮对话通过完整历史拼接、摘要保留和结构化注入三种方式利用之前的 Prompt,核心是在上下文窗口内最大化信息完整性。
Prompt管理:存什么?怎么存?相似度高怎么处理?
原始问法:
- Prompt管理:存什么?怎么存?相似度高怎么处理?
来源题目:
SRC-15-153-483
面试先答
Prompt 管理的核心是 存储有效 Prompt、快速检索复用、避免重复建设。存什么:存经过验证的高质量 Prompt 模板、对应的场景描述和效果评估数据。怎么存:通过 Prompt 注册表(存储元数据和版本)+ 向量索引(支持语义检索)+ 分类标签(支持分类浏览)的组合方案。相似度高的处理:通过 embedding 计算语义相似度,对相似 Prompt 进行聚类和版本管理,当新 Prompt 与已有 Prompt 相似度超过阈值时,提示复用或合并。核心目标是提升 Prompt 的复用率和迭代效率。
核心结论
- 存储内容:高质量 Prompt 模板 + 场景描述 + 效果数据 + 版本信息
- 存储方案:注册表 + 向量索引 + 分类标签
- 相似处理:embedding 相似度检测 + 聚类 + 版本管理
1. 存储内容设计
public class PromptTemplate {
private String id;
private String name;
private String template; // Prompt 模板
private String scenario; // 适用场景描述
private List<String> tags; // 分类标签
private Map<String, Object> metadata; // 元数据
private double avgScore; // 平均效果分
private int usageCount; // 使用次数
private String version; // 版本号
private List<String> versions; // 历史版本 ID
private float[] embedding; // 向量嵌入(用于相似度计算)
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
2. 存储方案
Prompt 注册表
@Service
public class PromptRegistry {
private final Map<String, PromptTemplate> prompts = new ConcurrentHashMap<>();
private final VectorStore vectorStore;
private final EmbeddingService embeddingService;
// 注册新 Prompt
public String register(PromptTemplate template) {
// 1. 检测相似度
float[] embedding = embeddingService.embed(template.getTemplate());
List<SimilarPrompt> similar = findSimilar(embedding, 0.85f);
if (!similar.isEmpty()) {
// 相似度超过阈值,提示复用
template.setSimilarPrompts(
similar.stream().map(SimilarPrompt::getId).toList()
);
}
// 2. 生成 ID 并存储
String id = generateId(template.getName());
template.setId(id);
template.setEmbedding(embedding);
prompts.put(id, template);
// 3. 存入向量索引
vectorStore.upsert(id, embedding, template.getMetadata());
return id;
}
// 相似度检测
public List<SimilarPrompt> findSimilar(float[] embedding, double threshold) {
return vectorStore.search(embedding, 10).stream()
.filter(result -> result.getScore() > threshold)
.map(result -> new SimilarPrompt(
result.getId(),
result.getScore()
))
.collect(Collectors.toList());
}
// 按场景/标签检索
public List<PromptTemplate> findByScenario(String scenario) {
return prompts.values().stream()
.filter(p -> p.getScenario().contains(scenario)
|| p.getTags().contains(scenario))
.sorted(Comparator.comparingInt(PromptTemplate::getUsageCount).reversed())
.collect(Collectors.toList());
}
}
3. 相似度处理策略
新 Prompt 提交
↓
计算 Embedding
↓
与已有 Prompt 比对相似度
↓
┌─────────────────────────────┐
│ 相似度 < 阈值 (0.7) │
│ → 注册为新 Prompt │
└─────────────┬───────────────┘
↓
┌─────────────────────────────┐
│ 0.7 ≤ 相似度 < 0.9 │
│ → 标记为相似,人工审核 │
│ → 可作为已有 Prompt 的新版本 │
└─────────────┬───────────────┘
↓
┌─────────────────────────────┐
│ 相似度 ≥ 0.9 │
│ → 拒绝注册,建议复用 │
│ → 自动关联到已有 Prompt │
└─────────────────────────────┘
4. 版本管理
public class PromptVersionManager {
// 创建新版本
public String createVersion(String promptId, String newTemplate) {
PromptTemplate original = registry.get(promptId);
// 保存旧版本
String oldVersionId = promptId + "-v" + original.getVersion();
registry.registerVersion(oldVersionId, original);
// 更新为新版本
original.setTemplate(newTemplate);
original.setVersion(incrementVersion(original.getVersion()));
original.getVersions().add(oldVersionId);
registry.update(original);
return original.getId();
}
// 获取指定版本
public PromptTemplate getVersion(String versionId) {
return registry.get(versionId);
}
}
5. 常见追问
- 追问 1:Prompt 效果如何评估?
- 通过 A/B 测试、用户反馈、任务完成率等指标持续评估
- 追问 2:如何构建 Prompt 分类体系?
- 按场景(客服/代码/分析)、按任务(生成/分类/提取)、按领域(金融/医疗/通用)多维分类
一句话总结
Prompt 管理通过注册表+向量索引+分类标签存储和检索高质量模板,通过 embedding 相似度检测处理重复,通过版本管理支持迭代,核心目标是提升复用率和迭代效率。
向量数据库如何选型?
原始问法:
- 向量数据库如何选型?
来源题目:
SRC-15-154-484
面试先答
向量数据库选型需要从 五大维度 综合评估:数据规模(量级决定存储架构,百万级选 Milvus/Qdrant,亿级选 Weaviate/Pinecone)、检索性能(QPS、延迟、召回率,核心看 ANN 索引算法选择)、功能需求(混合检索、元数据过滤、多模态支持、实时更新)、部署模式(云托管 vs 私有化部署 vs 嵌入式)、生态集成(与 LangChain/LlamaIndex 等框架集成度)。主流产品对比:Pinecone(全托管、简单易用)、Weaviate(混合检索强、支持多模态)、Milvus(开源、大规模场景)、Qdrant(Rust 编写、高性能)、FAISS(Facebook 开源库、嵌入式集成)。
核心结论
- 选型五维度:数据规模、检索性能、功能需求、部署模式、生态集成
- 主流产品:Pinecone(托管)、Weaviate(混合检索)、Milvus(大规模)、Qdrant(高性能)、FAISS(嵌入式)
- 核心原则:匹配业务场景优先,不过度追求功能
1. 主流产品对比
| 产品 | 核心优势 | 适用场景 | 部署模式 |
|---|---|---|---|
| Pinecone | 全托管、零运维 | 中小规模快速上线 | 云托管 |
| Weaviate | 混合检索、多模态 | 企业级 RAG 系统 | 云/私有化 |
| Milvus | 开源、亿级数据 | 大规模数据场景 | 私有化 |
| Qdrant | Rust 编写、高性能 | 对性能要求极高 | 私有化 |
| FAISS | Facebook 开源库 | 嵌入式集成、研究场景 | 嵌入式 |
| pgvector | PostgreSQL 插件 | 使用 PG 的现有系统 | 嵌入式 |
2. 选型决策流程
Step 1: 确定数据规模
├── < 100万 → FAISS / pgvector(嵌入式)
├── 100万 ~ 1000万 → Qdrant / Weaviate
└── > 1000万 → Milvus / Weaviate 集群
↓
Step 2: 确定部署需求
├── 不想运维 → Pinecone(全托管)
├── 需要私有化 → Milvus / Qdrant / Weaviate
└── 嵌入式 → FAISS / pgvector
↓
Step 3: 确定检索特性
├── 纯语义检索 → 所有产品都支持
├── 混合检索(关键词+语义)→ Weaviate / Milvus
├── 元数据过滤 → Weaviate / Qdrant / Milvus
└── 多模态 → Weaviate / Pinecone
↓
Step 4: 确定性能要求
├── 高 QPS + 低延迟 → Qdrant / Milvus
├── 平衡型 → Weaviate / Pinecone
└── 研究/学习 → FAISS
3. 关键评估指标
- 召回率(Recall):ANN 索引 vs 暴力搜索的结果比值,要求 ≥ 95%
- 查询延迟(Latency):P99 延迟,要求 < 50ms
- QPS(Queries Per Second):并发查询吞吐量
- 索引构建速度:全量索引构建时间
- 增量更新延迟:新数据写入到可检索的时间
4. 常见追问
- 追问 1:向量数据库和传统数据库的区别?
- 传统数据库存结构化数据,向量数据库存 embedding 向量,支持语义相似度检索
- 追问 2:什么时候用 pgvector 而不是独立向量数据库?
- 数据量不大(< 百万)、已有 PostgreSQL 基础设施、需要复杂事务支持时
一句话总结
向量数据库选型需综合数据规模、检索性能、功能需求、部署模式和生态集成五大维度,根据业务场景匹配最合适的产品。
检索召回速度怎么优化?
原始问法:
- 检索召回速度怎么优化?
来源题目:
SRC-15-154-485
面试先答
检索召回速度优化需要从 全链路 入手,涵盖 索引优化、查询优化、缓存策略、硬件加速 四个层面。索引优化 是核心:选择合适的 ANN 算法(HNSW 高召回、IVF 高速度、PQ 高压缩比)、调整索引参数(M、efSearch、nprobe)。查询优化:向量预归一化、批量查询、并行检索、限制候选集大小。缓存策略:热点向量缓存、最近查询结果缓存、分层缓存(L1 内存、L2 Redis、L3 磁盘)。硬件加速:GPU/TPU 加速向量计算、SIMD 指令优化、SSD 提升 I/O。实际优化需要根据场景在召回率和速度之间做 trade-off。
核心结论
- 四大优化方向:索引优化、查询优化、缓存策略、硬件加速
- 核心手段:ANN 索引调参 + 多级缓存
- Trade-off:召回率 vs 速度,需要根据场景平衡
1. 索引优化
ANN 算法选择
| 算法 | 代表实现 | 核心优势 | 适用场景 |
|---|---|---|---|
| HNSW | Milvus/Qdrant | 高召回率、稳定 | 中小规模、精度要求高 |
| IVF | FAISS/Weaviate | 高速度、可扩展 | 大规模、低延迟要求 |
| PQ | FAISS/Milvus | 高压缩比、低内存 | 内存受限、超大规模 |
| ScaNN | Google/FAISS | 高精度、高速度 | 通用场景 |
HNSW 参数调优
// HNSW 核心参数
HNSWConfig config = HNSWConfig.builder()
.setM(16) // 每层最大连接数,越大召回越高
.setEfConstruction(200) // 构建时搜索宽度
.setEfSearch(50) // 查询时搜索宽度,越大召回越高但越慢
.build();
// 调优策略:
// 追求速度 → M=8, efSearch=16
// 平衡型 → M=16, efSearch=50
// 追求召回 → M=32, efSearch=100
IVF 参数调优
// IVF 核心参数
IVFConfig config = IVFConfig.builder()
.setNlist(256) // 聚类中心数,越大分片越多、查询越快
.setNprobe(16) // 查询时搜索的聚类数,越大召回越高但越慢
.build();
// 调优策略:
// 速度优先 → nlist=1024, nprobe=4
// 平衡型 → nlist=256, nprobe=16
// 召回优先 → nlist=256, nprobe=64
2. 查询优化
public class OptimizedSearchService {
// 1. 向量预归一化
public float[] normalizeVector(float[] vector) {
float norm = 0;
for (float v : vector) norm += v * v;
norm = (float) Math.sqrt(norm);
if (norm > 0) {
for (int i = 0; i < vector.length; i++) {
vector[i] /= norm;
}
}
return vector;
}
// 2. 批量并行查询
public List<SearchResult> batchSearch(List<float[]> queries) {
return queries.parallelStream()
.map(q -> vectorStore.search(normalizeVector(q), topK))
.flatMap(List::stream)
.collect(Collectors.toList());
}
// 3. 限制候选集 + 重排序
public List<SearchResult> searchWithRerank(float[] query,
int candidateSize, int finalTopK) {
// 第一阶段:粗召回(大候选集)
List<SearchResult> candidates = vectorStore.search(query, candidateSize);
// 第二阶段:精排(重排序)
return reranker.rerank(query, candidates, finalTopK);
}
}
3. 缓存策略
public class LayeredCache {
private final Cache l1Cache; // 内存缓存(Caffeine)
private final RedisTemplate<String, List<SearchResult>> l2Cache;
public List<SearchResult> search(float[] query, String cacheKey) {
// L1: 内存缓存
List<SearchResult> cached = l1Cache.get(cacheKey);
if (cached != null) return cached;
// L2: Redis 缓存
cached = l2Cache.opsForValue().get(cacheKey);
if (cached != null) {
l1Cache.put(cacheKey, cached);
return cached;
}
// L3: 向量数据库查询
List<SearchResult> result = vectorStore.search(query, 100);
// 写入缓存
l1Cache.put(cacheKey, result);
l2Cache.opsForValue().set(cacheKey, result, 30, TimeUnit.MINUTES);
return result;
}
}
4. 常见追问
- 追问 1:如何平衡召回率和速度?
- 通过 efSearch/nprobe 等参数调节,在可接受的速度下最大化召回率
- 追问 2:冷热数据如何处理?
- 热数据缓存预热,冷数据延迟加载或归档
一句话总结
检索召回速度优化从索引、查询、缓存、硬件四方面入手,核心是选择合适的 ANN 算法和参数,配合多级缓存实现全链路优化。
ES千万级数据取top100,内部怎么执行?
原始问法:
- ES千万级数据取top100,内部怎么执行?
来源题目:
SRC-15-154-486
面试先答
ES 千万级数据取 Top 100 的内部执行流程分为 五个阶段:Query 阶段(客户端发送查询到协调节点)→ DFS 阶段(协调节点广播到所有分片,每个分片本地计算 Top N 候选和全局统计)→ Merge 阶段(协调节点汇总各分片的候选集,按评分排序)→ Fetch 阶段(根据最终 Top 100 的文档 ID,到对应分片拉取完整文档数据)→ Return 阶段(返回结果给客户端)。核心设计是 分片并行 + 两阶段查询:第一阶段轻量查询(只返回 ID 和评分),第二阶段按需拉取详情,避免大量数据传输。
核心结论
- ES 查询五步:Query → DFS → Merge → Fetch → Return
- 核心机制:分片并行 + 两阶段查询(轻量召回 + 按需拉取)
- 关键优化:分片路由、DFS 优化、Fetch 并行
1. 完整执行流程
客户端 协调节点 分片1 分片2 分片3
│ │ │ │ │
│── 1.Query ────────→│ │ │ │
│ │ │ │ │
│ │── 2.DFS(查询广播) ──→│ │ │
│ │ │ │ │
│ │ ┌───────────────┴──────────────┘ │
│ │ │ │
│ │ ↓ │
│ │── 2.DFS(查询广播) ───────────────────────────────→│
│ │ │ │ │
│ │ │ │←── 3.局部TopN ──│
│ │ │←── 3.局部TopN ──│ │
│ │←── 3.局部TopN ────│ │ │
│ │ │ │ │
│ │ ┌───────────────┴──────────────┐ │
│ │ │ │
│ │ ↓ │
│ │── 4.Merge(全局排序取Top100) │
│ │ │ │ │
│ │── 5.Fetch(按需拉取详情) ──────────────────────→│
│ │ │ │ │
│ │←── 5.Fetch(按需拉取详情) ───────────────────────│
│ │ │ │ │
│←── 6.Return ───────│ │ │ │
│ │ │ │ │
2. 关键步骤详解
DFS 阶段(Distributed Frequency Search)
每个分片执行:
1. 本地计算 BM25 评分
2. 取本地 Top N(默认 10 条)
3. 返回:文档 ID + 评分 + 全局统计(词频、文档数)
协调节点汇总:
1. 合并所有分片的 Top N 候选
2. 计算全局 BM25 评分(使用全局统计值)
3. 按全局评分排序,取 Top 100 的文档 ID 列表
Fetch 阶段
协调节点:
1. 根据 Top 100 文档 ID 列表,确定每个文档所在分片
2. 并行发送 Fetch 请求到对应分片
3. 分片返回完整文档数据
4. 协调节点汇总返回给客户端
3. 性能优化点
| 优化点 | 说明 | 效果 |
|---|---|---|
| 路由优化 | 按路由键分片,避免全分片扫描 | 减少分片扫描数量 |
| Top N 调优 | 增大分片级 Top N(10→50),提升召回 | 更高召回率 |
| DFS 缓存 | 缓存 DFS 阶段的统计信息 | 减少重复计算 |
| Fetch 并行 | 并行拉取多分片文档 | 减少等待时间 |
| Doc Values | 启用 Doc Values 加快排序和聚合 | 提升排序性能 |
| 分片数 | 设置合理分片数(= 数据节点数 × 3) | 最大化并行度 |
4. 常见追问
- 追问 1:Top N 调大或调小的影响?
- 调大:提升召回率但增加计算开销;调小:降低开销但可能漏召
- 追问 2:为什么不直接在每个分片取 Top 100?
- 会导致网络开销巨大,且合并后仍需重新排序
一句话总结
ES 千万级取 Top 100 通过分片并行的两阶段查询实现:第一阶段各分片本地取 Top N 返回 ID+评分,第二阶段协调节点合并排序后按需拉取详情,实现高效分布式检索。
检索这块做过效果上的优化吗?有什么优化案例?
原始问法:
- 检索这块做过效果上的优化吗?有什么优化案例?
来源题目:
SRC-15-154-487
面试先答
(注:以下为虚构案例,需替换成真实项目经历)我在之前的 RAG 系统项目中做过 检索效果的系统性优化,核心目标是将 Top 5 召回率从 72% 提升到 93%。主要做了 四个优化:Chunk 粒度优化(从固定 500 字符改为语义切分 + 50% 重叠,召回率 +8%)、混合检索策略(BM25 关键词 + 向量语义 + 重排序,召回率 +12%)、Query 扩展(同义词扩展 + 子问题分解,召回率 +5%)、效果评估闭环(构建 2000 条标注数据集,每周评估迭代)。最终效果:Top 5 召回率 72% → 93%,用户满意度从 68% → 89%。
核心结论
- (虚构案例)四维度优化:Chunk 粒度、混合检索、Query 扩展、评估闭环
- 效果提升:Top 5 召回率 72% → 93%
- 核心思路:数据驱动 + 多策略组合 + 持续迭代
1. 优化案例(虚构,需替换)
背景:某企业知识库 RAG 系统,用户反映回答不准确,经排查发现检索环节 Top 5 召回率仅 72%。
优化一:Chunk 粒度调整
// 优化前:固定 500 字符切分
public List<Chunk> fixedChunk(String text) {
return splitByFixedSize(text, 500);
}
// 优化后:语义切分 + 50% 重叠
public List<Chunk> semanticChunk(String text) {
List<Chunk> chunks = new ArrayList<>();
List<String> sentences = splitIntoSentences(text);
StringBuilder currentChunk = new StringBuilder();
int overlapSize = 0;
for (int i = 0; i < sentences.size(); i++) {
String sentence = sentences.get(i);
currentChunk.append(sentence);
// 达到目标大小或遇到语义边界
if (currentChunk.length() >= TARGET_SIZE
|| isSemanticBoundary(sentence)) {
chunks.add(new Chunk(currentChunk.toString(), i));
// 50% 重叠:保留最后几句
overlapSize = Math.max(3, sentences.size() / 10);
currentChunk = new StringBuilder();
for (int j = i - overlapSize + 1; j <= i; j++) {
currentChunk.append(sentences.get(j));
}
}
}
return chunks;
}
优化二:混合检索
public class HybridRetriever {
private final BM25Retriever bm25Retriever;
private final VectorRetriever vectorRetriever;
private final Reranker reranker;
public List<Document> retrieve(String query, int topK) {
// 1. BM25 关键词检索(取 topK*2)
List<Document> keywordResults = bm25Retriever.search(query, topK * 2);
// 2. 向量语义检索(取 topK*2)
List<Document> semanticResults = vectorRetriever.search(query, topK * 2);
// 3. 融合去重(RRF 算法)
List<Document> fused = rrfFusion(
keywordResults, semanticResults, 60
);
// 4. 重排序(Cross-Encoder 精排)
return reranker.rerank(query, fused, topK);
}
// Reciprocal Rank Fusion
private List<Document> rrfFusion(List<Document> list1,
List<Document> list2, int k) {
Map<String, Double> scores = new HashMap<>();
for (int i = 0; i < list1.size(); i++) {
scores.merge(list1.get(i).getId(),
1.0 / (k + i + 1), Double::sum);
}
for (int i = 0; i < list2.size(); i++) {
scores.merge(list2.get(i).getId(),
1.0 / (k + i + 1), Double::sum);
}
return scores.entrySet().stream()
.sorted(Map.Entry.<String, Double>comparingByValue().reversed())
.map(e -> getDocumentById(e.getKey()))
.collect(Collectors.toList());
}
}
优化三:Query 扩展
public class QueryExpander {
private final LlmService llmService;
private final ThesaurusService thesaurusService;
public String expand(String originalQuery) {
// 1. 同义词扩展
List<String> synonyms = thesaurusService.getSynonyms(originalQuery);
// 2. LLM 生成子问题
String subQuestions = llmService.generate(
"将以下问题分解为 3-5 个子问题:\n" + originalQuery
);
// 3. 组合扩展
return String.join(" OR ",
Stream.concat(
Stream.of(originalQuery),
synonyms.stream(),
Stream.of(subQuestions.split("\n"))
).collect(Collectors.toList())
);
}
}
优化四:评估闭环
public class EvaluationPipeline {
// 构建标注数据集
private static final List<QueryLabel> GROUND_TRUTH = loadLabels(
"evaluation_dataset.json" // 2000 条标注数据
);
public EvaluationResult evaluate() {
int correctTop1 = 0, correctTop5 = 0, correctTop10 = 0;
for (QueryLabel item : GROUND_TRUTH) {
List<Document> results = retriever.retrieve(item.getQuery(), 10);
Set<String> retrievedIds = results.stream()
.map(Document::getId).collect(Collectors.toSet());
if (results.get(0).getId().equals(item.getRelevantId())) {
correctTop1++;
}
if (retrievedIds.contains(item.getRelevantId())) {
correctTop5++;
}
}
return EvaluationResult.builder()
.top1Recall((double) correctTop1 / GROUND_TRUTH.size())
.top5Recall((double) correctTop5 / GROUND_TRUTH.size())
.top10Recall((double) correctTop10 / GROUND_TRUTH.size())
.build();
}
}
2. 优化效果
| 优化项 | Top 5 召回率 | 提升 |
|---|---|---|
| 基线 | 72% | - |
| + Chunk 优化 | 80% | +8% |
| + 混合检索 | 88% | +8% |
| + Query 扩展 | 91% | +3% |
| + 持续迭代 | 93% | +2% |
3. 常见追问
- 追问 1:优化过程中遇到的最大挑战?
- Chunk 粒度和检索精度的平衡,需要反复实验找到最优点
- 追问 2:如何向面试官证明这是你的真实经历?
- 用具体的数据指标、代码实现和决策过程来说明
一句话总结
检索效果优化需要从 Chunk 粒度、混合检索、Query 扩展和评估闭环四方面系统性推进,用数据驱动的方式持续迭代,最终实现召回率的显著提升。(以上为虚构案例,需替换成真实项目经历)
工具调用失败怎么办?
原始问法:
- 工具调用失败怎么办?
来源题目:
SRC-15-155-488
面试先答
Agent 工具调用失败是常态,需要 分层容错处理:即时重试(针对网络抖动等瞬时故障,指数退避重试 2-3 次)、降级策略(重试失败后切换到备用工具,如主数据库失败→只读副本)、保底方案(所有重试和降级都失败时,返回友好提示并建议用户手动操作)、错误上报(记录失败原因和上下文,供后续分析和模型微调)。核心设计原则是 "不让模型看到错误堆栈",而是将技术错误转化为自然语言反馈,让模型理解并尝试其他方案。同时需要区分 可重试错误(超时、网络抖动)和 不可重试错误(参数错误、业务规则不满足),对不同错误类型采取不同策略。
核心结论
- 四层容错:即时重试 → 降级切换 → 保底方案 → 错误上报
- 核心原则:将技术错误转为自然语言反馈,不让模型看到堆栈
- 关键设计:区分可重试和不可重试错误,差异化处理
1. 错误分类
public enum ToolErrorType {
// 可重试错误
NETWORK_TIMEOUT("网络超时", true),
CONNECTION_RESET("连接重置", true),
RATE_LIMIT("限流", true),
TEMPORARY_UNAVAILABLE("服务暂时不可用", true),
// 不可重试错误
INVALID_PARAMS("参数错误", false),
PERMISSION_DENIED("权限不足", false),
BUSINESS_RULE("业务规则不满足", false),
RESOURCE_NOT_FOUND("资源不存在", false),
UNKNOWN("未知错误", false);
private final String message;
private final boolean retryable;
}
2. 容错处理实现
@Service
public class ToolCallExecutor {
private static final int MAX_RETRIES = 3;
private static final long INITIAL_BACKOFF_MS = 100;
private final Map<String, Tool> toolRegistry;
private final ErrorReporter errorReporter;
public ToolResult executeWithFallback(ToolCall call) {
Tool tool = toolRegistry.get(call.getToolName());
// 1. 主工具尝试
ExecutionResult result = executeWithRetry(tool, call);
if (result.isSuccess()) {
return result.toToolResult();
}
// 2. 降级工具尝试
Tool fallbackTool = tool.getFallback();
if (fallbackTool != null) {
ExecutionResult fallbackResult = executeWithRetry(fallbackTool, call);
if (fallbackResult.isSuccess()) {
return fallbackResult.toToolResult();
}
}
// 3. 保底方案:返回自然语言提示
String friendlyMessage = buildFallbackMessage(result.getError());
errorReporter.report(call, result);
return ToolResult.failure(friendlyMessage);
}
private ExecutionResult executeWithRetry(Tool tool, ToolCall call) {
int attempts = 0;
long backoffMs = INITIAL_BACKOFF_MS;
while (attempts < MAX_RETRIES) {
try {
return tool.execute(call.getParams());
} catch (Exception e) {
attempts++;
ToolErrorType errorType = classifyError(e);
if (!errorType.isRetryable() || attempts >= MAX_RETRIES) {
return ExecutionResult.failure(errorType, e);
}
// 指数退避
sleep(backoffMs);
backoffMs *= 2;
}
}
return ExecutionResult.failure(ToolErrorType.UNKNOWN,
new RuntimeException("Max retries exceeded"));
}
private String buildFallbackMessage(ExecutionError error) {
return switch (error.getType()) {
case NETWORK_TIMEOUT -> "抱歉,操作暂时超时,请稍后重试或尝试其他方式";
case INVALID_PARAMS -> "参数可能有误,请确认后重试";
case PERMISSION_DENIED -> "权限不足,无法执行此操作";
case BUSINESS_RULE -> "操作不符合业务规则";
default -> "操作失败,请稍后重试";
};
}
}
3. 常见追问
- 追问 1:重试策略如何选择?
- 可重试错误用指数退避,不可重试错误直接返回
- 追问 2:如何避免重试风暴?
- 指数退避 + 最大重试次数 + 断路器模式
一句话总结
工具调用失败通过即时重试、降级切换、保底方案和错误上报四层容错处理,核心是将技术错误转化为自然语言反馈,让 Agent 有机会尝试其他方案。
怎么溯源工具调用失败?是意图识别错、入参错还是工具掉线?
原始问法:
- 怎么溯源工具调用失败?是意图识别错、入参错还是工具掉线?
来源题目:
SRC-15-155-489
面试先答
溯源工具调用失败需要 分层排查,从 Agent 决策层 → 工具调用层 → 工具实现层 逐层定位。第一步:看 Agent 日志,确认是否正确识别意图(Tool Selection),如果选了错误工具或没选工具 → 意图识别问题。第二步:看参数日志,确认 Agent 生成的入参是否符合工具 Schema 定义(Parameter Validation),如果参数缺失或类型错误 → 入参问题。第三步:看工具执行日志,如果参数正确但执行失败 → 工具实现问题(业务逻辑、数据库、外部依赖)。第四步:看基础设施,如果工具本身无异常 → 网络/服务发现/配置等基础设施问题。核心是建立完整的链路追踪日志。
核心结论
- 分层溯源:Agent 决策层 → 工具调用层 → 工具实现层 → 基础设施层
- 快速定位:完整链路追踪日志 + 结构化错误分类
- 关键能力:日志收集、实时监控、根因分析
1. 分层排查流程
工具调用失败
↓
┌─────────────────────────────┐
│ Layer 1: Agent 决策层 │
│ - 正确选择了工具? │
│ - 正确理解了用户意图? │
│ → 查看 tool_selection 日志 │
└─────────────┬───────────────┘
↓
意图识别正确?
↙ ↘
Yes No
↓ ↓
┌─────────────────────────────┐
│ Layer 2: 参数生成层 │
│ - 参数是否符合 Schema? │
│ - 是否有缺失/类型错误? │
│ → 查看 param_validation 日志 │
└─────────────┬───────────────┘
↓
参数生成正确?
↙ ↘
Yes No
↓ ↓
┌─────────────────────────────┐
│ Layer 3: 工具执行层 │
│ - 业务逻辑是否正常? │
│ - 外部依赖是否可用? │
│ → 查看 tool_execution 日志 │
└─────────────┬───────────────┘
↓
工具执行成功?
↙ ↘
Yes No
↓ ↓
┌─────────────────────────────┐
│ Layer 4: 基础设施层 │
│ - 网络是否正常? │
│ - 服务发现是否正常? │
│ - 配置是否正确? │
└─────────────────────────────┘
2. 链路追踪实现
@Service
public class ToolCallTracer {
private final TraceRepository traceRepo;
public void traceToolCall(ToolCallContext context) {
Trace trace = Trace.builder()
.traceId(UUID.randomUUID().toString())
.sessionId(context.getSessionId())
.userId(context.getUserId())
.toolName(context.getToolName())
.intent(context.getRecognizedIntent())
.params(context.getParams())
.selectionConfidence(context.getSelectionConfidence())
.status(context.getStatus())
.errorMessage(context.getErrorMessage())
.errorType(classifyError(context))
.timestamp(LocalDateTime.now())
.build();
traceRepo.save(trace);
}
// 错误分类器
private String classifyError(ToolCallContext context) {
if (!context.isToolSelected()) {
return "INTENT_MISRECOGNITION"; // 意图识别错误
}
if (!context.isParamValid()) {
return "PARAMETER_ERROR"; // 入参错误
}
if (!context.isToolHealthy()) {
return "TOOL_UNAVAILABLE"; // 工具掉线
}
if (!context.isBusinessLogicOk()) {
return "BUSINESS_LOGIC_ERROR"; // 业务逻辑错误
}
return "UNKNOWN";
}
}
3. 快速定位看板
@RestController
@RequestMapping("/api/trace")
public class TraceController {
private final ToolCallTracer tracer;
// 按 Trace ID 查询完整链路
@GetMapping("/{traceId}")
public TraceDetail getTrace(@PathVariable String traceId) {
return tracer.getFullTrace(traceId);
}
// 按 Session 查询所有调用
@GetMapping("/session/{sessionId}")
public List<Trace> getSessionTraces(
@PathVariable String sessionId,
@RequestParam(required = false) String errorType) {
return tracer.getBySession(sessionId, errorType);
}
// 统计分析
@GetMapping("/stats")
public TraceStats getStats(
@RequestParam LocalDateTime from,
@RequestParam LocalDateTime to) {
return tracer.getErrorStats(from, to);
}
}
4. 常见追问
- 追问 1:如何减少意图识别错误?
- 优化 System Prompt、增加 Few-shot 示例、引入意图分类器做二次确认
- 追问 2:如何快速定位线上问题?
- 通过 Trace ID 全链路追踪 + 实时监控大盘 + 结构化日志
一句话总结
工具调用失败溯源通过分层排查(Agent 决策→参数生成→工具执行→基础设施),配合完整链路追踪日志和结构化错误分类,能够快速定位是意图识别、入参还是工具本身的问题。
Agent系统可观测平台有哪些?LangSmith和Langfuse的区别?
原始问法:
- Agent系统可观测平台有哪些?LangSmith和Langfuse的区别?
来源题目:
SRC-15-155-490
面试先答
Agent 系统可观测平台主要有 LangSmith、Langfuse、PromptLayer、Arize、Helicone 等,它们提供 链路追踪、成本监控、效果评估、调试分析 四大核心能力。LangSmith 是 LangChain 官方产品,深度集成 LangChain 生态,优势在于 端到端的 Agent 调试(可视化 ReAct 循环、Playground 实时交互)和 数据集评估。Langfuse 是独立的开源可观测平台,优势在于 与框架无关(支持任何 LLM 应用)、轻量级、数据可自托管。简单选择建议:使用 LangChain 技术栈 → LangSmith;非 LangChain 技术栈或重视数据隐私 → Langfuse。
核心结论
- 主流平台:LangSmith、Langfuse、PromptLayer、Arize、Helicone
- 核心能力:链路追踪、成本监控、效果评估、调试分析
- LangSmith vs Langfuse:前者深度集成 LangChain,后者框架无关且开源
1. 平台能力对比
| 能力 | LangSmith | Langfuse | PromptLayer | Arize |
|---|---|---|---|---|
| 链路追踪 | ✅ 深度集成 | ✅ 通用 | ✅ 基础 | ✅ AI 原生 |
| 成本监控 | ✅ | ✅ | ✅ | ✅ |
| 效果评估 | ✅ 数据集评估 | ✅ 评分机制 | ✅ | ✅ 模型对比 |
| Playground | ✅ 实时交互 | ❌ | ❌ | ❌ |
| 开源 | ❌ | ✅ | ❌ | ❌ |
| 自托管 | ❌ | ✅ | ❌ | ✅ |
| 框架支持 | LangChain 为主 | 通用 | 通用 | 通用 |
2. LangSmith 核心功能
// LangSmith 集成(LangChain 项目)
@Configuration
public class LangSmithConfig {
@Bean
public Runnable langSmithTracer() {
return LangSmithTracer.builder()
.projectName("my-agent-project")
.apiKey(apiKey)
.build();
}
// 自动追踪 Agent 执行
@Bean
public Agent tracedAgent(ChatModel model, List<Tool> tools) {
return Agent.builder()
.model(model)
.tools(tools)
.tracer(langSmithTracer())
.build();
}
}
LangSmith 界面功能
- Trace View:可视化 Agent 的 ReAct 循环,每一步思考和工具调用
- Playground:实时调试 Prompt 和参数,对比不同模型效果
- Datasets:创建测试数据集,批量评估 Agent 效果
- Evaluations:自动化评估 Agent 输出质量(正确率、相关性等)
- Curation:标注和修复低质量的 Agent 轨迹
3. Langfuse 核心功能
// Langfuse 集成(框架无关)
@Configuration
public class LangfuseConfig {
@Bean
public LangfuseClient langfuseClient() {
return Langfuse.builder()
.publicKey(publicKey)
.secretKey(secretKey)
.host("https://cloud.langfuse.com") // 自托管地址
.build();
}
// 手动追踪任意 LLM 调用
public String traceAgentExecution(TraceContext context) {
return langfuseClient.trace(
Trace.builder()
.name("agent-execution")
.sessionId(context.getSessionId())
.userId(context.getUserId())
.input(context.getInput())
.build()
).getId();
}
// 记录 LLM 调用详情
public void recordLlmCall(String traceId, LlmCall call) {
langfuseClient.generation(
Generation.builder()
.traceId(traceId)
.model(call.getModel())
.prompt(call.getPrompt())
.completion(call.getResponse())
.cost(call.getCost())
.latency(call.getLatency())
.build()
);
}
}
4. 选型建议
技术栈判断
↓
┌─────────────────────────┐
│ 使用 LangChain/LlamaIndex│
│ → LangSmith(深度集成) │
└───────────┬─────────────┘
↓
┌─────────────────────────┐
│ 非 LangChain 技术栈 │
├─────────────────────────┤
│ 数据需要自托管? │
│ → Yes: Langfuse(开源) │
│ → No: LangSmith / Langfuse│
└───────────┬─────────────┘
↓
┌─────────────────────────┐
│ 需要 Playground 调试? │
│ → Yes: LangSmith │
│ → No: Langfuse / 其他 │
└─────────────────────────┘
5. 常见追问
- 追问 1:可观测平台和 APM 工具(Jaeger、Zipkin)的区别?
- APM 面向传统应用,可观测平台面向 LLM/Agent 应用,理解 LLM 特有概念(Token、Prompt、Tool Call)
- 追问 2:自建可观测平台需要什么?
- 至少需要:LLM 调用日志、工具调用日志、评估框架、可视化看板
一句话总结
LangSmith 和 Langfuse 是 Agent 可观测领域的两大主流平台,前者深度集成 LangChain 生态,后者框架无关且开源,根据技术栈和数据隐私需求选择即可。
工具重试不成功怎么办?保底工具策略?
原始问法:
- 工具重试不成功怎么办?保底工具策略?
来源题目:
SRC-15-155-491
面试先答
当工具重试全部失败后,需要 三级保底策略:第一级:同类型替代工具(如 MySQL 失败→PostgreSQL,Redis 主库失败→只读从库)、第二级:降级能力(如完整报告生成失败→简化版摘要,如文件上传失败→返回下载链接)、第三级:人工介入(返回友好错误信息 + 建议用户操作 + 自动创建工单通知运维)。设计原则是 核心能力不降级、辅助能力可降级:支付等核心操作必须返回明确失败,搜索结果等辅助操作可以返回部分结果。同时需要 熔断机制:连续 N 次失败后自动停止调用该工具,一段时间内直接走降级路径。
核心结论
- 三级保底:同类型替代 → 能力降级 → 人工介入
- 设计原则:核心能力不降级,辅助能力可降级
- 熔断机制:连续失败自动停止调用,走降级路径
1. 保底策略架构
用户请求
↓
┌─────────────────────────────┐
│ 主工具调用 │
│ (自动重试 2-3 次) │
└─────────────┬───────────────┘
↓ 失败
┌─────────────────────────────┐
│ Level 1: 同类型替代工具 │
│ MySQL → PostgreSQL │
│ Redis 主 → Redis 从 │
│ 搜索 API A → 搜索 API B │
└─────────────┬───────────────┘
↓ 失败
┌─────────────────────────────┐
│ Level 2: 能力降级 │
│ 完整报告 → 摘要报告 │
│ 实时数据 → 缓存数据 │
│ 完整功能 → 基础功能 │
└─────────────┬───────────────┘
↓ 失败
┌─────────────────────────────┐
│ Level 3: 人工介入 │
│ → 返回友好错误信息 │
│ → 建议用户操作 │
│ → 创建工单通知运维 │
└─────────────────────────────┘
2. 实现代码
@Service
public class GuaranteedToolExecutor {
private final CircuitBreaker circuitBreaker;
private final AlertService alertService;
public ToolResult execute(ToolCall call) {
String toolName = call.getToolName();
// 检查熔断状态
if (circuitBreaker.isOpen(toolName)) {
return executeFallback(call);
}
// Level 0: 主工具 + 重试
ToolResult result = executeWithRetry(call);
if (result.isSuccess()) return result;
// Level 1: 替代工具
ToolResult alternative = tryAlternativeTool(call);
if (alternative.isSuccess()) {
logFallback(call, "alternative_tool");
return alternative;
}
// Level 2: 能力降级
ToolResult degraded = tryDegradedCapability(call);
if (degraded.isSuccess()) {
logFallback(call, "degraded_capability");
return degraded;
}
// Level 3: 人工介入
circuitBreaker.recordFailure(toolName);
alertService.createTicket(call, result.getError());
return buildHumanInterventionResult(call);
}
// 替代工具映射
private ToolResult tryAlternativeTool(ToolCall call) {
Map<String, String> alternatives = Map.of(
"mysql_query", "postgresql_query",
"redis_get", "redis_get_replica",
"search_web", "search_web_backup"
);
String altTool = alternatives.get(call.getToolName());
if (altTool != null) {
return executeWithRetry(call.toBuilder().toolName(altTool).build());
}
return ToolResult.failure("No alternative tool available");
}
// 降级能力映射
private ToolResult tryDegradedCapability(ToolCall call) {
Map<String, DegradedAction> degradedMap = Map.of(
"generate_report", new DegradedAction("generate_summary",
"生成简化版摘要报告"),
"realtime_data", new DegradedAction("get_cached_data",
"使用最近缓存的数据"),
"full_search", new DegradedAction("basic_search",
"使用基础搜索功能")
);
DegradedAction action = degradedMap.get(call.getToolName());
if (action != null) {
ToolResult result = executeWithRetry(
call.toBuilder().toolName(action.getToolName()).build()
);
if (result.isSuccess()) {
result.setMessage(
result.getMessage() + "\n(已降级:" + action.getDescription() + ")"
);
}
return result;
}
return ToolResult.failure("No degraded capability available");
}
}
3. 熔断器实现
@Component
public class CircuitBreaker {
private final Map<String, CircuitState> states = new ConcurrentHashMap<>();
private static final int FAILURE_THRESHOLD = 5;
private static final long RECOVERY_TIMEOUT_MS = 60000;
public boolean isOpen(String toolName) {
CircuitState state = states.get(toolName);
if (state == null) return false;
if (state.getFailureCount() >= FAILURE_THRESHOLD) {
long elapsed = System.currentTimeMillis() - state.getLastFailureTime();
if (elapsed > RECOVERY_TIMEOUT_MS) {
state.setState(CircuitState.HALF_OPEN);
return false;
}
return true; // 熔断中
}
return false;
}
public void recordFailure(String toolName) {
states.computeIfAbsent(toolName, k -> new CircuitState())
.recordFailure();
}
public void recordSuccess(String toolName) {
states.computeIfAbsent(toolName, k -> new CircuitState())
.recordSuccess();
}
}
4. 常见追问
- 追问 1:降级后如何通知用户?
- 在返回结果中明确标注"已降级",说明原因和影响
- 追问 2:如何防止降级变成常态?
- 设置监控告警,连续降级时自动通知运维
一句话总结
工具重试失败后通过替代工具、能力降级、人工介入三级保底策略确保服务可用,配合熔断器防止故障扩散,核心原则是核心能力不降级、辅助能力可降级。
入参校验怎么做?有什么现成手段?
原始问法:
- 入参校验怎么做?有什么现成手段?
来源题目:
SRC-15-155-492
面试先答
Agent 工具入参校验需要 多层校验体系,从 模型层 → 框架层 → 工具层 → 服务层 逐层保障。模型层:通过 System Prompt 和 Few-shot 引导模型生成正确参数。框架层:利用 Function Calling 的 Schema 自动校验(参数名、类型、必填项)。工具层:自定义业务规则校验(值范围、格式、业务约束)。服务层:最终的安全校验(注入防护、权限检查、数据合法性)。现成手段包括 JSON Schema 校验、Bean Validation(@Valid)、Hibernate Validator、Spring Validation。核心是 "不信任模型输出",模型生成的参数必须经过严格校验才能执行。
核心结论
- 四层校验:模型层引导 → 框架层 Schema → 工具层业务规则 → 服务层安全
- 现成手段:JSON Schema、Bean Validation、Hibernate Validator
- 核心原则:不信任模型输出,所有参数必须校验
1. 校验体系架构
模型生成参数
↓
┌─────────────────────────────┐
│ Layer 1: Prompt 引导 │
│ - System Prompt 说明参数格式 │
│ - Few-shot 提供正确示例 │
└─────────────┬───────────────┘
↓
┌─────────────────────────────┐
│ Layer 2: Schema 自动校验 │
│ - JSON Schema 定义参数规范 │
│ - Function Calling 自动校验 │
│ → 类型错误/必填缺失直接拦截 │
└─────────────┬───────────────┘
↓
┌─────────────────────────────┐
│ Layer 3: 业务规则校验 │
│ - 值域检查(min/max) │
│ - 格式检查(正则) │
│ - 业务约束(交叉校验) │
└─────────────┬───────────────┘
↓
┌─────────────────────────────┐
│ Layer 4: 安全校验 │
│ - SQL 注入防护 │
│ - 权限检查 │
│ - 数据合法性 │
└─────────────────────────────┘
2. 各层实现
Layer 1: Prompt 引导
// 在 System Prompt 中明确参数格式要求
String systemPrompt = """
你是一个客服助手。调用工具时必须严格遵守参数格式:
工具名:query_order
参数:
- order_id (string, 必填): 订单号,格式为ORD+10位数字
- date_range (object, 可选):
- start_date (string): YYYY-MM-DD
- end_date (string): YYYY-MM-DD
示例:
正确:{"order_id": "ORD1234567890"}
错误:{"order_id": "123"} // 缺少ORD前缀
""";
Layer 2: JSON Schema 校验
public class ToolSchemaValidator {
private final JsonSchemaFactory schemaFactory;
public ValidationResult validate(String toolName,
Map<String, Object> params) {
JsonNode schema = loadSchema(toolName);
JsonNode paramsNode = objectMapper.valueToTree(params);
Set<ValidationMessage> errors = schema.validate(paramsNode);
if (errors.isEmpty()) {
return ValidationResult.valid();
}
return ValidationResult.invalid(
errors.stream()
.map(ValidationMessage::getMessage)
.collect(Collectors.toList())
);
}
private JsonNode loadSchema(String toolName) {
String schemaJson = toolSchemaRegistry.get(toolName);
return objectMapper.readTree(schemaJson);
}
}
Layer 3: 业务规则校验
public class BusinessRuleValidator {
public List<String> validate(String toolName,
Map<String, Object> params) {
List<String> errors = new ArrayList<>();
switch (toolName) {
case "query_order":
validateOrderId(params.get("order_id"), errors);
validateDateRange(params.get("date_range"), errors);
break;
case "transfer_money":
validateAmount(params.get("amount"), errors);
validateAccount(params.get("account"), errors);
break;
// ... 其他工具
}
return errors;
}
private void validateOrderId(Object orderId, List<String> errors) {
if (orderId == null) {
errors.add("order_id 是必填项");
return;
}
if (!orderId.toString().matches("ORD\\d{10}")) {
errors.add("order_id 格式错误,应为ORD+10位数字");
}
}
private void validateAmount(Object amount, List<String> errors) {
if (amount == null || new BigDecimal(amount.toString()).compareTo(BigDecimal.ZERO) <= 0) {
errors.add("amount 必须大于 0");
}
if (new BigDecimal(amount.toString()).compareTo(new BigDecimal("1000000")) > 0) {
errors.add("单次转账金额不能超过100万");
}
}
}
Layer 4: 安全校验
public class SecurityValidator {
public void validate(Map<String, Object> params, String userId) {
// SQL 注入防护
for (Map.Entry<String, Object> entry : params.entrySet()) {
if (entry.getValue() instanceof String value) {
if (containsSqlInjection(value)) {
throw new SecurityException("检测到潜在的 SQL 注入风险");
}
}
}
// 权限检查
if (!hasPermission(userId, params)) {
throw new SecurityException("无权限执行此操作");
}
}
private boolean containsSqlInjection(String input) {
String[] patterns = {
"(?i)(\\b(SELECT|INSERT|UPDATE|DELETE|DROP)\\b)",
"(?i)(--|;|\\/\\*)",
"(?i)(\\b(OR|AND)\\b.*\\b(TRUE|FALSE)\\b)"
};
for (String pattern : patterns) {
if (input.matches(pattern)) return true;
}
return false;
}
}
3. 常见追问
- 追问 1:校验失败后怎么办?
- 返回校验错误信息给 Agent,让其修正参数后重试
- 追问 2:如何设计 Tool Schema?
- 参考 OpenAPI Spec,定义清晰的参数名、类型、约束、描述
一句话总结
入参校验通过 Prompt 引导、Schema 自动校验、业务规则校验和安全校验四层体系保障,核心是不信任模型输出,所有参数必须经过验证才能执行。
日常使用哪些AI编程工具?
原始问法:
- 日常使用哪些AI编程工具?
来源题目:
SRC-15-156-493
面试先答
日常使用的 AI 编程工具可以分为 四类:IDE 集成类(GitHub Copilot、Cursor、Amazon CodeWhisperer)——最主流,直接在编辑器中补全和生成代码;聊天助手类(ChatGPT、Claude、Codeium)——通过对话形式解决编程问题;CLI 工具类(Aider、AutoGPT、Meta 的 Code Llama)——在命令行中批量生成和修改代码;专业领域类(TestGPT 生成测试、CodeRabbit Code Review、Mintify 生成文档)——针对特定编程环节。我的日常组合是 Copilot 做实时补全 + Cursor 做代码重构 + Claude 做疑难解答,三者配合覆盖不同场景。
核心结论
- 四类工具:IDE 集成、聊天助手、CLI 工具、专业领域
- 主流组合:Copilot(补全)+ Cursor(重构)+ Claude(解答)
- 核心价值:提升编码效率、降低重复劳动、辅助学习
1. 工具分类对比
| 类别 | 代表工具 | 核心能力 | 适用场景 |
|---|---|---|---|
| IDE 集成 | GitHub Copilot | 行级代码补全、函数生成 | 日常编码、重复模式 |
| IDE 集成 | Cursor | 项目级代码理解、重构 | 代码重构、跨文件修改 |
| IDE 集成 | Amazon CodeWhisperer | 多语言支持、安全扫描 | AWS 技术栈开发 |
| 聊天助手 | ChatGPT/Claude | 代码解释、方案设计 | 疑难问题、学习新技术 |
| 聊天助手 | Codeium | 免费、多 IDE 支持 | 预算有限的开发者 |
| CLI 工具 | Aider | 批量文件修改、Git 集成 | 大规模重构、批量修改 |
| CLI 工具 | AutoGPT | 自主完成编程任务 | 自动化编程任务 |
| 专业工具 | TestGPT | 自动生成单元测试 | 测试覆盖率提升 |
| 专业工具 | CodeRabbit | 自动化 Code Review | 代码质量保障 |
2. 场景化推荐
日常编码
↓
GitHub Copilot → 实时补全(Tab 接受)
↓
遇到新 API/框架
↓
Claude/ChatGPT → 询问用法、看示例
↓
需要重构/跨文件修改
↓
Cursor → 选中代码,描述需求,自动修改
↓
需要批量修改
↓
Aider → 命令行批量替换
↓
代码完成
↓
TestGPT → 自动生成测试
CodeRabbit → 自动 Review
3. 常见追问
- 追问 1:Copilot 和 Cursor 的区别?
- Copilot 是行级补全,Cursor 是项目级理解和操作
- 追问 2:AI 生成的代码需要审查吗?
- 必须审查,AI 可能生成看似正确但逻辑有缺陷的代码
一句话总结
日常使用 AI 编程工具覆盖 IDE 集成补全、聊天助手解答、CLI 批量操作和专业领域辅助四大类,根据不同场景组合使用最大化效率。
不同场景怎么切换使用AI编程工具?
原始问法:
- 不同场景怎么切换使用AI编程工具?
来源题目:
SRC-15-156-494
面试先答
不同场景下 AI 编程工具的选择遵循 "复杂度匹配" 原则:单行/小片段生成 → Copilot(Tab 补全,最快);函数级生成 → Copilot 或 Codeium(自然语言描述函数意图);跨文件重构 → Cursor(选中区域 + 描述需求 + 自动修改);方案设计/疑难解答 → Claude/ChatGPT(对话式,获取思路);批量修改 → Aider(CLI 模式,一次改多个文件);学习新技术 → Claude(生成教程+示例+最佳实践)。关键是 不要用锤子解决螺丝问题:简单补全用 Copilot,复杂重构用 Cursor,不确定的问 Claude。
核心结论
- 场景匹配原则:复杂度匹配工具能力
- 选择路径:Copilot(简单)→ Cursor(复杂)→ Claude(不确定)
- 核心效率:选对工具比用好工具更重要
1. 场景决策矩阵
| 场景 | 推荐工具 | 操作方式 | 示例 |
|---|---|---|---|
| 行级补全 | Copilot | 输入前几个字符,Tab 接受 | 输入 for → Tab 生成循环 |
| 函数生成 | Copilot/Cursor | 写注释描述意图 → 生成 | // 将List按age分组 → 生成 groupingBy |
| 跨文件修改 | Cursor | 选中代码 + 描述需求 | 选中 UserService → "将所有方法改为异步" |
| 代码重构 | Cursor/Aider | 描述重构目标 | "将User类拆分为User和UserProfile" |
| 疑难解答 | Claude/GPT | 对话提问 | "为什么 ConcurrentHashMap 会这样?" |
| 学习新技术 | Claude/GPT | 请求教程 | "用通俗语言解释 CompletableFuture" |
| 批量生成 | Aider | CLI 指令 | aider "为所有Service添加@Async注解" |
| 测试生成 | TestGPT | 选中方法 → 生成测试 | 选中 calculate() → 生成 JUnit 测试 |
| Code Review | CodeRabbit | 自动 Review PR | 提交 PR → 自动评论建议 |
2. 实际工作流示例
场景:从 0 到 1 开发一个 Spring Boot 接口
Step 1: 创建 Controller 类
→ Copilot: 输入 @RestController 后自动生成模板
→ 输入方法签名 + 注释,Tab 生成方法体
Step 2: 实现 Service 层
→ Copilot: 接口定义后自动生成实现类骨架
→ 复杂逻辑:描述意图,Copilot 生成初版
Step 3: 需要重构(如将同步改异步)
→ Cursor: 选中整个 Service 类
→ 描述:"将所有方法改为 CompletableFuture 异步返回"
→ Cursor 自动修改相关代码
Step 4: 不确定某个 API 用法
→ Claude: "Spring 的 @Async 注解怎么用?线程池怎么配置?"
→ 获取方案后回到 IDE 实现
Step 5: 生成单元测试
→ TestGPT: 选中 Service 类 → 生成测试
→ 手动补充 Mock 数据和边界用例
Step 6: Code Review
→ CodeRabbit: 提交 PR → 自动 Review
→ 根据建议修改代码
3. 常见追问
- 追问 1:如何避免过度依赖 AI?
- 保持代码审查习惯,理解每一行生成的代码
- 追问 2:团队如何统一工具使用?
- 制定 AI 编程规范,定期分享最佳实践
一句话总结
根据场景复杂度匹配不同 AI 编程工具:简单补全用 Copilot,复杂操作用 Cursor,疑难解答用 Claude,核心是选对工具而非盲目追求最强工具。
AI Coding的流程是什么?大项目需求怎么解决?
原始问法:
- AI Coding的流程是什么?大项目需求怎么解决?
来源题目:
SRC-15-156-495
面试先答
AI Coding 的完整流程分为 六步:需求理解(明确输入输出和约束)→ 方案设计(选择技术方案和架构)→ 代码生成(按模块/函数逐步生成)→ 代码审查(AI 审查 + 人工审查)→ 测试验证(生成测试 + 运行验证)→ 迭代优化(根据反馈调整)。大项目需求的核心解法是 分而治之:将大需求拆解为小任务(Epic → Story → Task),每个小任务独立走 AI Coding 流程,通过 多轮对话 + 上下文管理 确保各模块一致性。关键是 先设计后编码,用 AI 辅助设计再逐步实现。
核心结论
- AI Coding 六步:需求→设计→生成→审查→测试→迭代
- 大项目解法:分而治之 + 上下文管理 + 渐进式实现
- 核心原则:先设计后编码,结构化拆解
1. 完整流程
Step 1: 需求理解
├── 明确功能边界
├── 定义输入输出
└── 识别约束条件
↓
Step 2: 方案设计
├── 选择技术栈
├── 设计模块架构
├── 定义接口契约
└── 数据模型设计
↓
Step 3: 代码生成(按模块)
├── 3.1 基础设施层(配置、工具类)
├── 3.2 数据层(Entity、Repository)
├── 3.3 业务层(Service、DTO)
├── 3.4 接口层(Controller、API)
└── 3.5 横切关注点(日志、异常、安全)
↓
Step 4: 代码审查
├── AI 自动审查(CodeRabbit)
├── 人工审查(代码规范、业务逻辑)
└── 安全审查(注入、越权等)
↓
Step 5: 测试验证
├── 单元测试(TestGPT 生成 + 补充)
├── 集成测试
└── 运行验证
↓
Step 6: 迭代优化
├── Bug 修复
├── 性能优化
└── 代码重构
2. 大项目分而治之策略
// 需求拆解示例:开发一个电商订单系统
//
// Epic: 订单管理系统
// ├── Story 1: 创建订单
// │ ├── Task 1.1: 定义 Order 数据模型
// │ ├── Task 1.2: 实现 OrderRepository
// │ ├── Task 1.3: 实现 OrderService.createOrder()
// │ └── Task 1.4: 实现 OrderController.POST /orders
// ├── Story 2: 查询订单
// │ ├── Task 2.1: 实现 OrderService.queryOrder()
// │ └── Task 2.2: 实现 OrderController.GET /orders/{id}
// ├── Story 3: 支付订单
// │ ├── Task 3.1: 集成支付网关
// │ └── Task 3.2: 实现支付回调处理
// └── Story 4: 取消订单
// └── Task 4.1: 实现取消逻辑和状态流转
3. 上下文管理关键
public class ProjectContextManager {
// 项目级上下文:确保各模块一致性
private String projectContext;
public String buildProjectContext() {
return """
项目:电商订单系统
技术栈:Spring Boot 3 + JPA + Redis
规范:
- Controller → Service → Repository 分层
- DTO/Entity 分离
- 统一异常处理
- RESTful API 设计
- 统一响应封装
""";
}
// 每个模块生成时注入项目上下文
public String generateWithContext(String module, String requirement) {
return projectContext + """
现在实现模块:%s
需求:%s
请严格遵循项目规范生成代码。
""".formatted(module, requirement);
}
}
4. 常见追问
- 追问 1:AI 生成的代码质量如何保障?
- 设计先行 + 分模块生成 + 代码审查 + 测试覆盖
- 追问 2:大项目中如何避免 AI "忘记"前面的设计?
- 通过项目级上下文注入 + 模块化迭代 + 定期回顾设计文档
一句话总结
AI Coding 遵循需求→设计→生成→审查→测试→迭代六步流程,大项目通过分而治之拆解为小任务,配合上下文管理确保各模块一致性,核心是先设计后编码。
怎么保证AI生成代码的正确性和质量?
原始问法:
- 怎么保证AI生成代码的正确性和质量?
来源题目:
SRC-15-156-496
面试先答
保证 AI 生成代码的正确性和质量需要 五道防线:第一道:Prompt 约束(在生成时明确代码规范、边界条件、性能要求);第二道:静态分析(IDE 实时检查、SonarQube 扫描、编译错误检测);第三道:单元测试(为 AI 生成的代码编写/生成单元测试,确保逻辑正确);第四道:Code Review(人工审查代码质量、设计合理性、安全性);第五道:运行验证(集成测试、性能测试、安全扫描)。核心原则是 "AI 生成,人来验证",AI 是助手不是替代品,最终质量由人保障。
核心结论
- 五道防线:Prompt 约束 → 静态分析 → 单元测试 → Code Review → 运行验证
- 核心原则:AI 生成,人来验证
- 关键习惯:理解每一行代码、测试覆盖、审查到位
1. 五道防线详解
第一道:Prompt 约束
// 高质量 Prompt 约束示例
String codingPrompt = """
请生成一个用户认证的 Service 类,要求:
1. 代码规范:
- 使用 Lombok 简化代码
- 方法名驼峰,类名大驼峰
- 异常使用自定义 BusinessException
- 添加 Javadoc 注释
2. 功能要求:
- register(UserDTO): 注册用户,校验邮箱唯一
- login(LoginDTO): 登录验证,返回 JWT
- refreshToken(String): 刷新 Token
3. 边界条件:
- 邮箱格式校验
- 密码强度校验(>=8位,含数字和字母)
- 登录失败次数限制(5次锁定30分钟)
4. 性能要求:
- 密码加密使用 BCrypt
- 敏感操作添加日志
""";
第二道:静态分析
<!-- Maven 集成 SonarQube -->
<plugin>
<groupId>org.sonarsource.scanner.maven</groupId>
<artifactId>sonar-maven-plugin</artifactId>
<configuration>
<sonar.host.url>http://sonarqube:9000</sonar.host.url>
<sonar.login>${SONAR_TOKEN}</sonar.login>
</configuration>
</plugin>
第三道:单元测试
// 为 AI 生成的 Service 编写测试
@Test
public void testRegisterSuccess() {
UserDTO dto = new UserDTO();
dto.setEmail("test@example.com");
dto.setPassword("Pass123456");
assertDoesNotThrow(() -> userService.register(dto));
User savedUser = userRepository.findByEmail("test@example.com");
assertNotNull(savedUser);
assertNotEquals("Pass123456", savedUser.getPassword());
}
@Test
public void testRegisterDuplicateEmail() {
// 边界条件:重复邮箱
UserDTO dto = new UserDTO();
dto.setEmail("existing@example.com");
assertThrows(BusinessException.class,
() -> userService.register(dto));
}
@Test
public void testPasswordStrength() {
// 边界条件:弱密码
UserDTO dto = new UserDTO();
dto.setEmail("test@example.com");
dto.setPassword("123"); // 不符合强度要求
assertThrows(BusinessException.class,
() -> userService.register(dto));
}
第四道:Code Review 检查清单
| 检查项 | 说明 | AI 常见问题 |
|---|---|---|
| 逻辑正确性 | 业务逻辑是否正确 | 边界条件遗漏 |
| 安全性 | 注入、越权、泄露 | SQL 注入风险 |
| 性能 | N+1 查询、大对象 | 未加索引、循环查询 |
| 代码规范 | 命名、格式、注释 | 命名不一致 |
| 设计模式 | 分层、抽象、耦合 | 过度耦合或过度设计 |
| 异常处理 | 异常类型、日志 | 吞掉异常、日志不足 |
第五道:运行验证
// 集成测试
@SpringBootTest
class UserIntegrationTest {
@Autowired
private WebTestClient webClient;
@Test
void testFullAuthFlow() {
// 注册 → 登录 → 刷新 → 登出
UserDTO user = new UserDTO("test@example.com", "Pass123456");
webClient.post().uri("/api/users/register")
.bodyValue(user)
.exchange()
.expectStatus().isOk();
LoginDTO login = new LoginDTO("test@example.com", "Pass123456");
String token = webClient.post().uri("/api/users/login")
.bodyValue(login)
.exchange()
.expectStatus().isOk()
.expectBody(LoginResponse.class)
.returnResult().getResponseBody().getToken();
// 验证 Token 有效性
webClient.get().uri("/api/users/profile")
.header("Authorization", "Bearer " + token)
.exchange()
.expectStatus().isOk();
}
}
2. 常见追问
- 追问 1:如何区分 AI 生成的代码和自己写的?
- 无法区分,关键是质量而非来源
- 追问 2:如何快速审查 AI 代码?
- 关注边界条件、异常处理、性能问题三个高风险区域
一句话总结
通过 Prompt 约束、静态分析、单元测试、Code Review 和运行验证五道防线,系统性保障 AI 生成代码的正确性和质量,核心是人来做最终验证。
Cursor从设计稿直接生成代码的挑战是什么?
原始问法:
- Cursor从设计稿直接生成代码的挑战是什么?
来源题目:
SRC-15-156-497
面试先答
(注:以下为基于技术原理的分析,非 Cursor 内部实现细节)从设计稿直接生成代码的核心挑战有 五大类:设计稿理解问题(设计稿是视觉表达,缺乏精确的布局坐标、响应式规则、交互状态等代码层面的信息)、技术栈选择(设计稿不知道用 React/Vue/原生还是小程序,需要推断或手动指定)、组件识别(识别按钮、表单、列表等 UI 组件并映射到具体组件库)、交互逻辑缺失(设计稿通常只展示静态页面,缺乏点击、跳转、数据加载等交互描述)、工程化适配(生成的代码需要符合项目的代码规范、目录结构、组件库选择)。当前的解决方案通常需要 人工指定技术栈 + 选择组件库 + 补充交互逻辑,AI 辅助生成骨架代码。
核心结论
- 五大挑战:设计稿理解、技术栈选择、组件识别、交互逻辑、工程化适配
- 当前状态:AI 辅助生成骨架,人工补充细节
- 发展方向:多模态理解 + 设计 Tokens 结构化
1. 挑战详解
挑战一:设计稿理解不精确
设计稿(Figma)表达的是:
- 视觉布局(相对位置、视觉层级)
- 颜色/字体/间距(设计 Tokens)
- 组件形状(圆形按钮、方形卡片)
但缺乏代码层面的精确信息:
- 响应式断点(手机/平板/桌面)
- Flex/Grid 布局方式
- 具体像素值或百分比
- 组件层级和嵌套关系
挑战二:技术栈不确定
设计稿 → ??? 技术栈
├── React + Tailwind + shadcn/ui
├── Vue 3 + Element Plus
├── 小程序 + 原生组件
└── Flutter + Material
需要用户指定或 AI 推断,但容易出错
挑战三:组件识别与映射
设计稿元素 → 目标组件库
─────────────────────────────────
矩形按钮 → Button(variant: primary)
输入框 → Input(type: text)
卡片容器 → Card + CardHeader + CardBody
列表项 → List + ListItem
下拉菜单 → Select / Dropdown
弹窗 → Modal / Dialog
表单验证 → Form + FormItem + rules
挑战四:交互逻辑缺失
设计稿通常展示的状态:
- 默认状态(Default)
- 悬停状态(Hover)
- 选中状态(Selected)
- 禁用状态(Disabled)
但缺失的交互逻辑:
- 点击后触发什么?
- 数据从哪里加载?
- 提交后跳转到哪里?
- 表单验证规则是什么?
- 权限控制在哪里?
挑战五:工程化适配
生成的代码需要适配:
- 项目目录结构(src/components、src/pages)
- 路由配置(React Router、Vue Router)
- 状态管理(Redux、Pinia、Zustand)
- API 调用约定(axios 封装、React Query)
- 代码规范(ESLint、Prettier、Husky)
- 国际化支持(i18n)
2. 当前解决方案
设计稿(Figma)
↓
Step 1: 导出设计 Tokens
→ 颜色、字体、间距、圆角
→ 生成 CSS Variables / Tailwind Config
↓
Step 2: 指定技术栈
→ 用户选择 React/Vue/小程序
→ 指定 UI 组件库(shadcn/ui、Element Plus)
↓
Step 3: AI 生成骨架代码
→ 识别组件结构
→ 生成组件骨架和样式
→ 映射到指定组件库
↓
Step 4: 人工补充交互
→ 添加事件处理
→ 实现数据绑定
→ 配置路由
→ 添加状态管理
↓
Step 5: 工程化调整
→ 移动到正确目录
→ 统一代码规范
→ 集成到项目结构
3. 发展趋势
| 方向 | 说明 | 预期突破 |
|---|---|---|
| 多模态理解 | AI 直接理解设计稿的精确布局 | 减少设计信息丢失 |
| Design Tokens 标准 | 统一的设计 Token 格式(如 DTC) | 精确映射样式 |
| 组件库预设 | 预定义技术栈+组件库映射 | 减少人工选择 |
| 交互自动推断 | 根据设计模式推断交互逻辑 | 补充交互缺失 |
| 工程化模板 | 预定义项目工程化模板 | 自动适配项目结构 |
4. 常见追问
- 追问 1:Figma 的代码导出功能和 AI 生成有什么区别?
- Figma 导出精确但不智能,AI 生成更智能但不够精确,理想是结合使用
- 追问 2:什么时候用设计稿生成代码?
- 快速原型验证、重复性页面、UI 基础组件生成
一句话总结
从设计稿生成代码面临设计理解不精确、技术栈不确定、组件识别、交互缺失和工程化适配五大挑战,当前通过 AI 辅助生成骨架代码 + 人工补充细节的模式实现,未来向多模态理解方向发展。