旅行助手 Agent+SFT 测试题答案


一、概念理解

1. Agent vs 普通 LLM

核心区别:普通 LLM 只能生成文本,Agent = LLM + 工具调用 + 决策循环。

Agent 的工作循环:

  1. 感知:接收用户输入
  2. 决策:LLM 判断意图,决定是否需要调用工具
  3. 行动:调用工具(API、数据库等)获取外部信息
  4. 观察:接收工具返回结果
  5. 回复/继续:如果信息足够则生成最终回答,否则继续循环调用更多工具

2. SFT 的作用

基座模型虽有语言能力,但不知道:

  • 何时该调用工具(判断意图)
  • 调用哪个工具(从5个中选择)
  • 传什么参数(构造正确的 JSON)
  • 如何串联多个工具(如酒店推荐→评价的链式调用)
  • 何时该拒绝回答

SFT 通过大量标注好的对话示范,让模型学会以上所有技能,本质是行为克隆


3. LoRA 原理

LoRA 核心思想:不直接更新大矩阵 W,而是学习一个低秩分解 ΔW = A × B,推理时 W’ = W + ΔW。

计算:

  • (a) 全量微调:1024 × 1024 = 1,048,576 个参数
  • (b) LoRA:A(1024×32) + B(32×1024) = 32,768 + 32,768 = 65,536 个参数
  • (c) 节省倍数:1,048,576 / 65,536 = 16 倍

4. RAG vs SFT

RAGSFT
解决的问题知识问题——模型不知道的信息能力问题——模型不会的行为
本项目中提供 200+ 城市的旅行攻略内容教模型学会调用工具的格式和时机

互补关系:SFT 教模型"我应该调用 search_travel_guide 来查攻略",RAG 确保这个函数能从向量库中检索到有用的攻略内容。没有 SFT,模型不会调工具;没有 RAG,工具返回空内容。


5. Function Calling 完整流程

  1. 系统把工具定义(JSON Schema)和用户消息一起发给 LLM
  2. LLM 分析用户意图,输出结构化的 JSON(工具名 + 参数),而非自然语言
  3. 外部程序(非 LLM)解析这个 JSON
  4. 外部程序调用对应的真实函数/API,获取结果
  5. 把工具结果以 {"role": "tool", "content": result} 的形式追加到对话中
  6. 再次调用 LLM,LLM 基于工具结果生成最终回答

关键:LLM 自己不执行任何函数,它只是输出调用指令,实际执行由外部程序完成。


6. RRF 混合检索

RRF 公式:rrf_score(d) = Σ 1/(k + rank_i)

文档 A 的计算:

  • 向量检索排名 2:1/(60+2) = 1/62 ≈ 0.01613
  • 关键词检索排名 5:1/(60+5) = 1/65 ≈ 0.01538
  • RRF 分数 = 0.01613 + 0.01538 ≈ 0.03151

RRF 的优势:不需要归一化不同检索方式的分数(向量相似度和 TF 分数量纲不同),只看排名,简单有效。


二、项目理解

7. 数据流水线

步骤脚本说明
1generate_dataset.py基于100个城市模板 + LLM 生成1010条原始数据
2convert_dataset_final_fixed.py用 TravelAssistant 实际执行工具调用,生成完整对话
3merge_json_files.py合并所有 batch 文件
4split_dataset.py划分训练集和测试集
5conversation_splitter.py拆分多轮对话 + 最后一轮 3x 增强
6最终输出merged_train_final_multiturn_v2.json(19MB)

8. 五大工作流

(a) 工作流 3(酒店推荐)最复杂,原因:

  • 需要连续调用两个工具:先 recommend_hotelsget_hotel_reviews
  • 两次调用之间不能中断(不能先回复用户再查评价)
  • 模型需要学会自动链式调用,而非等待用户下一轮指令

(b) 工作流 5(拒绝)重要性:

  • 防止模型对非旅行问题"幻觉回答"
  • 定义了模型的能力边界
  • 如果没有拒绝样本,模型可能对任何问题都尝试调用工具,造成误调用和资源浪费

9. “只训练最后一轮"策略

配合方式:

  1. conversation_splitter.py 把一条 N 轮对话拆成 N 个训练样本
  2. 每个样本的最后一轮 assistant 都不同(分别是第1轮、第2轮…第N轮)
  3. 训练时 only_last_assistant=True 只计算每个样本最后一轮的 loss

为什么比训练所有轮次更好:

  • 避免重复梯度:第1轮 assistant 在样本1中已经被当作最后一轮训练过,在样本2中再训练一次是浪费
  • 聚焦当前轮:每个样本只关注"在给定上下文下,下一步应该怎么做”
  • 结合 3x 增强,最终的完整回答获得更大权重

10. 工具定义

(a) search_mode 可选值:"vector"(向量搜索)、"keyword"(关键词搜索)、"hybrid"(混合搜索),默认 "hybrid"

(b) query_route 的唯一必填参数:end_location(终点地址)。起点可以使用默认坐标。

(c)num_days 而非 end_date 的原因:

  • 更直觉:用户说"玩3天"比说"到4月3日"更常见
  • 避免日期计算错误:LLM 计算日期差容易出错
  • API 兼容:和风天气 API 按天数查询更方便

11. 数据分布设计

(a) 旅行规划占比最高(450/1010 ≈ 44.6%)原因:

  • 这是最核心、最常用的功能
  • 涉及两个工具的链式调用(攻略+天气),复杂度高
  • 需要更多样本让模型学好

(b) 设计"信息不足需追问"的数据原因:

  • 真实场景中用户经常不说完整信息(如只说"我想旅游",不说去哪)
  • 教模型学会主动提问补全信息,而不是瞎猜
  • 这提升了用户体验和安全性(不会根据不完整信息胡乱调工具)

三、代码分析

12. LoRA 配置分析

(a) lora_alpha / r = 2 是缩放因子。实际更新量 = ΔW × (alpha/r) = ΔW × 2。比值越大,LoRA 层的影响越大。2 是经验推荐值,平衡了学习能力和稳定性。

(b) 同时覆盖注意力层和 FFN 层的原因:

  • 注意力层(q/k/v/o_proj):学习"关注什么"——即理解何时该调用工具
  • FFN 层(gate/up/down_proj):学习"生成什么"——即输出正确的工具参数 JSON
  • 只改注意力层不够,因为工具调用需要精确的输出格式

(c) r 从 32 改为 4 的影响:

  • 参数量减少 8 倍,训练更快
  • 但学习容量大幅降低,可能无法学会 5 个工具的完整调用模式
  • 简单任务(如拒绝)可能还行,但复杂的链式调用(酒店工作流)可能退化

13. 训练超参数

(a) 有效 batch size = per_device_train_batch_size × gradient_accumulation_steps = 1 × 8 = 8

(b) gradient_checkpointing

  • 作用:前向传播时不保存所有中间激活值,反向传播时重新计算,用时间换显存
  • 代价:训练速度慢约 20-30%(因为需要重新计算)
  • 好处:显存占用减少约 60%,使得单卡能训练更大模型

(c) LoRA 学习率更大的原因:

  • LoRA 只训练一小部分新增参数(A 和 B 矩阵),这些参数是从零初始化
  • 全量微调是在已有权重上微调,太大的学习率会破坏预训练知识
  • LoRA 的低秩结构天然限制了更新幅度,所以可以用更大的学习率而不会过拟合

14. Agent 循环代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
while True:
    response = llm.chat(messages, tools=tools)

    if response.tool_calls:
        for tool_call in response.tool_calls:
            # (1) 执行工具调用,获取结果
            result = execute_tool(tool_call.function.name,
                                  json.loads(tool_call.function.arguments))
            # (2) 将工具结果追加到消息列表
            messages.append({
                "role": "tool",
                "tool_call_id": tool_call.id,
                "content": json.dumps(result)
            })
        # (3) 继续循环,让 LLM 基于工具结果继续决策(不 break)
        continue
    else:
        print(response.content)
        break

15. 数据去重

(a) 需要去重的原因:LLM 在生成工具调用时可能在多轮中重复调用同一个工具(相同参数),特别是在酒店工作流中,模型可能重复调用 recommend_hotels

(b) 去重 key 的构造:f"{tool_call.function.name}:{tool_call.function.arguments}",即工具名 + 参数字符串的拼接。只有完全相同的调用才会被去重。

(c) 不去重的后果:

  • 训练数据中包含冗余的工具调用
  • 模型学会"一个问题调用两次同样的工具"这种错误模式
  • 推理时浪费 API 调用,增加延迟和成本
  • 可能导致重复结果混入最终回答

四、优化方案

16. RAG 优化

优化方向 1:分块策略

  • 当前:每个城市一整篇攻略存为一条记录
  • 改进:按段落/章节分块(如"景点"、“美食”、“交通"分开),每块 300-500 字,相邻块有 50 字重叠
  • 理由:整篇文档太长(可能上千字),检索时无法精确定位用户关心的部分。分块后,查"杭州美食"只返回美食相关段落,减少无关信息干扰。

优化方向 2:加入 Reranker

  • 当前:RRF 融合后直接返回 top-k
  • 改进:RRF 初筛 top-20 → Reranker(如 bge-reranker-v2)精排 → 返回 top-5
  • 理由:RRF 只看排名不看内容,Reranker 能基于 query-document 语义精确打分。

其他可选方向

  • 动态调整检索阈值(基于 query 置信度)
  • 定期更新攻略数据保持时效性
  • 使用开源 Embedding(如 BGE-M3)降低 API 成本

17. 推理部署优化

(a) 延迟优化

  • 工具并行调用:旅行规划中 search_travel_guideget_weather_info 可以并行执行(当前是串行),减少约 50% 的工具调用等待时间
  • 或者:使用 KV Cache + 投机解码加速推理

(b) 吞吐量优化

  • 使用 vLLM 部署模型,支持 Continuous Batching(连续批处理),多个用户请求共享 GPU 计算
  • PagedAttention 避免 KV Cache 的显存碎片

(c) 容错/降级

  • 工具调用失败时的降级策略:如天气 API 超时 → 返回"建议查看当地天气预报"而非报错
  • RAG 检索无结果时 → LLM 基于自身知识生成通用建议,并提示"信息可能不完全准确”
  • 设置工具调用超时和重试机制

18. 评估体系设计

(a) 评估指标(至少 3 个):

  1. 工具选择准确率:模型选对了正确的工具(如旅行规划应该选 search_travel_guide
  2. 参数准确率:工具参数是否正确(如城市名、日期格式、天数)
  3. 调用完整率:是否调用了所有必需的工具(如酒店工作流需要连续调两个)
  4. 拒绝准确率:非旅行问题是否正确拒绝
  5. 端到端回答质量:最终回答是否合理整合了工具结果(可用 LLM-as-Judge)

(b) 测试集样本类型

  • 各工作流的标准样本(正例)
  • 信息不完整的样本(测试追问能力)
  • 边界样本(半旅行半非旅行的问题)
  • 纯非旅行样本(测试拒绝能力)
  • 需要链式调用的复杂样本

(c) 判断 Function Calling “正确"的标准

  • 工具名完全匹配
  • 所有必填参数都存在且格式正确
  • 参数值语义正确(如用户说"北京”,参数里也是"北京"而非"上海")
  • 不应该有多余的工具调用

五、综合设计题

19. 扩展 book_ticket 工具

(a) JSON Schema 定义

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
{
    "type": "function",
    "function": {
        "name": "book_ticket",
        "description": "预订火车票或机票",
        "parameters": {
            "type": "object",
            "properties": {
                "departure_city": {
                    "type": "string",
                    "description": "出发城市,如'北京'"
                },
                "arrival_city": {
                    "type": "string",
                    "description": "到达城市,如'上海'"
                },
                "departure_date": {
                    "type": "string",
                    "description": "出发日期,格式YYYY-MM-DD"
                },
                "ticket_type": {
                    "type": "string",
                    "description": "票种:train(火车)、flight(飞机)",
                    "enum": ["train", "flight"]
                },
                "passenger_count": {
                    "type": "integer",
                    "description": "乘客人数,默认1",
                    "default": 1
                }
            },
            "required": ["departure_city", "arrival_city", "departure_date", "ticket_type"]
        }
    }
}

(b) 需要修改的文件

  1. all_tools.json — 添加上述工具定义
  2. tools/ 目录 — 新建 book_ticket.py,实现订票 API 调用
  3. travel_assistant_funcall_fixed.py — 添加工作流 6(订票流程),在系统提示中描述何时触发
  4. generate_dataset.py — 新增订票场景的数据生成模板
  5. convert_dataset_final_fixed.py — 添加对 book_ticket 工具的执行逻辑

(c) 训练数据需求

  • 直接订票:200 条(“帮我订明天北京到上海的机票”)
  • 需追问:50 条(“帮我订票” → “请问您要从哪里出发?")
  • 与旅行规划组合:100 条(“帮我规划北京游,顺便订机票”)
  • 拒绝不合理请求:30 条(“帮我订去火星的票”)

(d) 新工作流设计

是的,需要定义工作流 6:订票服务

  1. 提取出发地、目的地、日期、交通方式
  2. 如信息不完整,主动追问
  3. 调用 book_ticket 查询票务信息
  4. 返回可选班次、价格、时间
  5. (可选)与工作流 1 联动:规划完行程后自动建议订票