行业信息助手 — 测试题答案

⚠️ 请先独立作答,再查阅此文件!

答案文件对应 [[行业信息助手_学习笔记]] 第十节测试题。


基础概念(理解层)

题 1:RAG 与纯 LLM 问答的本质区别

核心区别:

对比项纯 LLMRAG
知识来源训练时的参数记忆外部检索的实时文档
私有数据无法访问可以访问
知识更新需要重新训练更新文档库即可
幻觉风险较高(无依据瞎编)较低(有原文依据)

本项目 RAG 涉及的模块:

  1. document_service.py — 文档解析与分块(PDF/DOCX → 文本片段)
  2. embedding_service.py — 文本片段 → 向量(调用 DashScope Embedding API)
  3. milvus_service.py — 向量存储与相似度检索
  4. retrieval_service.py — 混合检索(向量检索 + 关键词),支持 Reranker 重排序
  5. chat_service.py / chat_service_v2.py — 将检索结果拼入 prompt 发给 LLM

题 2:为什么需要 6 个 Agent

原因:单一 LLM 的局限性

一次 LLM 调用需要同时完成:搜索判断、信息提取、代码编写、逻辑写作、质量评估——这些任务对模型的提示词、输出格式、温度参数需求截然不同,放在一起会互相干扰,质量下降。

专业分工的好处:

  • 提示词专精:每个 Agent 的 system prompt 针对单一任务优化
  • 参数调优:DataAnalyst 用低温(0.3)保证数据准确,LeadWriter 用高温(0.7)保证文章流畅
  • 并行潜力:搜索和数据提取可以并行执行
  • 质量可控:CriticMaster 独立审核,避免「自己写自己评」的偏差
  • 职责清晰:出错时容易定位到哪个 Agent 的问题

题 3:SSE vs WebSocket,为什么选 SSE

区别:

对比项SSEWebSocket
方向单向(服务端 → 客户端)双向
协议HTTP/1.1独立协议(需握手升级)
重连浏览器自动重连需手动实现
复杂度简单较复杂
适用场景推送通知、进度流实时聊天、游戏

本项目选 SSE 的理由: 深度研究是单向流——用户发起请求后,只需要接收服务端的进度更新,不需要在研究过程中向服务端发消息。SSE 恰好满足这个场景,实现更简单,且 FastAPI 原生支持 StreamingResponse,无需额外依赖。


题 4:Checkpoint 解决的问题与双状态设计

解决的问题: 深度研究耗时可能长达数分钟。网络中断、浏览器关闭或服务器重启都会导致研究进度全部丢失,用户不得不重新开始。

两种状态分开存储的原因:

状态字段内容恢复时用途
后端状态state_json已收集的事实、搜索结果、各 Agent 输出让 LangGraph 从中断的节点继续执行
前端状态ui_state_json进度条、已展示的中间结果、搜索关键词列表恢复页面显示,让用户看到之前的进展

分开设计的原因:后端状态是执行逻辑的依据,前端状态是展示的依据,二者结构和用途不同。如果混在一起,后端 Agent 需要了解 UI 细节,违反关注点分离原则。


架构分析(分析层)

题 5:完整深度研究请求的数据流

 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
36
37
38
39
40
41
42
用户点击「开始研究」,输入问题 Q
[前端] 发送 POST /research/stream(带 session_id + query)
[后端 research_router.py] 接收请求,创建 StreamingResponse
[deep_research_v2/service.py] 初始化 ResearchState,调用 LangGraph 图
[graph.py - 节点1] ChiefArchitect.run()
  → 调用 DashScope LLM,生成报告大纲(章节列表)
  → yield SSE: {phase: "PLANNING", outline: [...]}
  → 前端更新进度 UI
[graph.py - 节点2] DeepScout.run()
  → 对每个章节生成搜索关键词
  → 调用 web_search_service.py(Bocha/Serper API)
  → 调用 retrieval_service.py(Milvus 向量检索)
  → yield SSE: {phase: "RESEARCHING", results: [...]}
[graph.py - 节点3] DataAnalyst.run()
  → 从搜索结果中提取量化数据(数字、百分比、时间)
  → 生成 Fact 和 DataPoint 对象列表
[graph.py - 节点4] CodeWizard.run()
  → 根据 DataPoint 生成 matplotlib Python 代码
  → 执行代码,生成图表文件(PNG/Base64)
  → yield SSE: {phase: "VISUALIZING", charts: [...]}
[checkpoint_service.py] 保存当前 state 到 PostgreSQL
[graph.py - 节点5] LeadWriter.run()
  → 整合大纲、事实、图表,撰写完整报告 Markdown
  → yield SSE: {phase: "WRITING", content: "..."}
[graph.py - 节点6] CriticMaster.run()
  → 评分(0-10),生成修改建议
  → 若 score < 6.0 → 跳回 DeepScout 节点(继续搜索)
  → 若 score ≥ 6.0 → 进入 COMPLETED
yield SSE: {phase: "COMPLETED", report: "..."}
[前端] EventSource 接收,渲染最终报告 Markdown

题 6:不同 Agent 温度参数的设计原因

温度(temperature)的含义: 控制 LLM 输出的随机性。温度越低,输出越确定、保守;温度越高,输出越多样、有创意。

Agent温度任务性质为什么这个温度
DataAnalyst0.3(低)提取数字、验证事实数据提取需要准确性,不能「创意性地」修改数字,低温减少幻觉
CodeWizard0.3(低)生成 Python 代码代码必须语法正确、逻辑严谨,高温会产生莫名其妙的代码
DeepScout0.5(中)搜索策略、关键词生成需要一定创意来发散搜索角度,但也不能太离谱
CriticMaster0.5(中)质量评审评审需要客观,但也需要一定灵活性来识别不同类型的问题
ChiefArchitect0.7(高)规划大纲大纲设计需要创意和发散性思维,不同研究问题需要不同结构
LeadWriter0.7(高)撰写报告写作需要流畅自然的语言,低温会导致文章生硬重复

题 7:Redis 和 Milvus 的职责,能否互换

Redis 的职责:

  • 缓存频繁查询的数据(新闻、搜索结果)
  • 存储「取消研究」标志位(cancel_flag:{session_id}
  • 用户 Session 缓存
  • 本质:键值缓存,适合存小型结构化数据,读写极快

Milvus 的职责:

  • 存储文档的向量表示(高维浮点数组)
  • 执行向量相似度搜索(ANN 近似最近邻)
  • 本质:向量数据库,专为高维向量的相似度计算优化

能否互换?不能。

  • Redis 不支持高维向量的相似度检索(没有 ANN 索引)
  • Milvus 不适合做普通键值缓存(查询方式是向量相似度,不是精确键查找)
  • 两者解决的是完全不同的问题

题 8:为什么提供 GET 版本的研究接口

GET /research/stream 的存在原因:

  1. 浏览器测试便利:在浏览器地址栏直接输入 URL 就能测试 SSE 流,不需要 Postman 或代码
  2. EventSource 限制:浏览器原生的 EventSource API 只支持 GET 请求,如果前端想用原生 EventSource(而非 fetch + ReadableStream),必须提供 GET 接口
  3. 开发调试:GET 接口方便开发者快速验证 SSE 连接是否正常
📝 补充

本项目前端可能使用 fetch + ReadableStream 来消费 POST 的 SSE,GET 版本主要是调试用途。


代码理解(应用层)

题 9:新增 LegalAnalyst Agent 需要修改的文件

至少需要修改以下 5 个位置

1. 新建 Agent 文件 backend/app/service/deep_research_v2/agents/legal_analyst.py

1
2
3
4
5
6
from .base import BaseAgent

class LegalAnalyst(BaseAgent):
    async def run(self, state: ResearchState) -> ResearchState:
        # 搜索法规政策,更新 state.legal_findings
        ...

2. agents/__init__.py

1
from .legal_analyst import LegalAnalyst  # 新增导出

3. config/llm_config.py

1
2
3
4
5
6
7
class AgentsConfig:
    # 新增 legal_analyst 配置
    legal_analyst = AgentModelConfig(
        model="deepseek-v3.2",
        temperature=0.3,  # 法规分析需要严谨
        max_tokens=4000
    )

4. service/deep_research_v2/state.py

1
2
3
class ResearchState(TypedDict):
    # 新增字段存储法规分析结果
    legal_findings: List[str]

5. service/deep_research_v2/graph.py

1
2
3
4
# 新增节点并连接到图中
graph.add_node("legal_analyst", legal_analyst.run)
graph.add_edge("data_analyst", "legal_analyst")  # 数据分析后做法规分析
graph.add_edge("legal_analyst", "wizard")

题 10:用 Valtio 设计研究进度状态

 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
36
37
38
39
40
41
42
43
44
45
46
47
// store/research.ts
import { proxy } from 'valtio'

interface ResearchStep {
  id: string
  phase: 'PLANNING' | 'RESEARCHING' | 'ANALYZING' | 'WRITING' | 'REVIEWING' | 'COMPLETED'
  label: string
  status: 'pending' | 'running' | 'done' | 'error'
  detail?: string
}

export const researchStore = proxy({
  // 进度(0-100)
  progress: 0,

  // 当前阶段
  currentPhase: '' as ResearchStep['phase'],

  // 各步骤状态
  steps: [] as ResearchStep[],

  // 最终报告
  report: '',

  // 是否正在研究
  isRunning: false,

  // 更新进度(直接修改 proxy 对象即可触发 UI 更新)
  updateProgress(phase: ResearchStep['phase'], detail?: string) {
    const phaseProgress: Record<ResearchStep['phase'], number> = {
      PLANNING: 10, RESEARCHING: 30, ANALYZING: 55,
      WRITING: 75, REVIEWING: 90, COMPLETED: 100
    }
    this.progress = phaseProgress[phase]
    this.currentPhase = phase
    const step = this.steps.find(s => s.phase === phase)
    if (step) {
      step.status = phase === 'COMPLETED' ? 'done' : 'running'
      if (detail) step.detail = detail
    }
  },

  setReport(content: string) {
    this.report = content
    this.isRunning = false
  }
})
1
2
3
4
5
6
7
8
9
// 在 Chat 页面消费 SSE 并更新 store
const source = new Response(/* fetch SSE */)
for await (const chunk of source) {
  const event = JSON.parse(chunk)
  researchStore.updateProgress(event.phase, event.detail)
  if (event.phase === 'COMPLETED') {
    researchStore.setReport(event.report)
  }
}

在 React 组件中使用 useSnapshot 订阅:

1
2
3
4
5
6
import { useSnapshot } from 'valtio'

function ProgressBar() {
  const snap = useSnapshot(researchStore)
  return <Progress percent={snap.progress} status={snap.isRunning ? 'active' : 'normal'} />
}

扩展思考(评估层)

题 11:迭代次数改为 5 可能带来的问题

潜在问题:

  1. 成本爆炸:每次迭代调用 DeepScout(搜索 API)+ DataAnalyst + LeadWriter(16000 token)+ CriticMaster。5 轮迭代的 token 消耗和 API 费用可能是 1 轮的 4-5 倍。

  2. 时间过长:每轮至少几十秒,5 轮可能需要 5-10 分钟,用户体验极差,且 HTTP 连接超时风险大。

  3. 收益递减:CriticMaster 的评分标准是 LLM 给的,本身有随机性。第 2-3 次迭代的改进可能边际效益很小,第 4-5 次甚至可能因为「过度修改」质量下降(LLM 漂移问题)。

  4. 无限循环风险:如果 CriticMaster 评分标准不稳定(比如对同一报告时而 5.5 时而 6.5),在阈值附近来回震荡,可能把 5 次迭代全部用光。

  5. 内存压力:每次迭代累积的 state(搜索结果、事实列表)越来越大,可能超出 LLM 上下文窗口或服务器内存(已有 MEM_LIMIT=8G 限制)。

建议的改进方案: 保留迭代次数上限,但加入「改进量阈值」:如果两次评分差 < 0.5,即使未达到 6.0 也提前终止,避免无效迭代。


题 12:stock_mapping.py 硬编码的局限与改进方案

硬编码的局限性:

  1. 维护困难:新上市公司、公司改名、退市需要手动更新代码并重新部署
  2. 覆盖不全:目前只有约 90 家公司,A 股 5000+ 家公司无法全覆盖
  3. 无版本管理:不知道映射数据是什么时候的,是否过时
  4. 无容错:公司名称有多种写法(“华为” vs “华为技术”),硬编码无法处理模糊匹配

更好的设计方案:

方案一:数据库表 + 定期同步

1
2
3
4
5
6
7
8
CREATE TABLE stock_mapping (
    id SERIAL PRIMARY KEY,
    company_name VARCHAR(100),
    stock_code VARCHAR(10),
    exchange VARCHAR(10),  -- 'SH', 'SZ', 'BJ'
    aliases TEXT[],        -- 别名数组,支持模糊匹配
    updated_at TIMESTAMP
);
  • 通过股票数据 API(如聚合数据)每天定时同步全量数据
  • 查询时走数据库,支持模糊匹配(LIKE 或全文检索)

方案二:调用专业金融 API

  • 实时查询第三方股票数据服务(如通达信、同花顺 API)
  • 不需要本地维护映射,始终最新
  • 缺点:依赖外部服务,有 API 成本

方案三:LLM 辅助 + 缓存

  • 用 LLM 识别公司名称并返回股票代码
  • 结果缓存到 Redis,下次直接用
  • 适合长尾场景(小众公司)