跳到主要内容

04 · 搜索与来源:把四家抹平成一种形状

这一章讲数据整流:四个搜索后端 API 长得完全不同,项目怎么把它们统一成一种结果形状,再去重、按 token 预算截断、可选抓全文。这是让上层节点「不关心用哪家搜索」的关键。

4.1 统一的结果形状

不管用哪个后端,web_research 拿到的都是同一种字典(utils.py 各搜索函数的返回):

# 示意,非源码——统一后的形状
{
"results": [
{
"title": "标题",
"url": "来源链接",
"content": "摘要/片段",
"raw_content": "全文(或 None)",
},
...
]
}

四个后端各自负责把自家字段翻译到这四个键上:

后端需要 key原字段 → 统一字段位置
DuckDuckGo无(免费)href/title/bodyurl/title/contentutils.py:166
SearXNG自建实例 URLlink/title/snippeturl/title/contentutils.py:223
TavilyAPI key本身就接近统一形状,直接返回utils.py:278
PerplexityAPI key一段答复 + 一串 citations → 拆成多条utils.py:310

DuckDuckGo 和 SearXNG 还做了完整性校验:三个字段缺任何一个就跳过该条并打印告警(utils.py:198-200/258-260),避免把残缺来源塞进下游。

4.2 Perplexity 的特别处理

Perplexity 的 sonar-pro 返回的是一段综合答复 + 一串引用链接,不是逐条结果。项目把它拆开(utils.py:363-387):第一条 citation 挂上完整答复文本(raw_content 给全文),其余 citation 只作为引用链接、raw_content 设为 None、content 写「See above for full content」。这样它就能塞进统一形状里,同时避免把整段答复复制很多份。

4.3 去重 + 格式化 + token 预算

拿到统一结果后,deduplicate_and_format_sources(utils.py:55-120)做三件事:

一、按 URL 去重。 用字典以 url 为键,重复 URL 只留第一条(utils.py:95-98)。

二、拼成结构化文本。 每条来源拍成 Source: / URL: / Most relevant content: 三段(utils.py:102-107),方便 LLM 阅读。

三、按 token 预算截断全文。fetch_full_page 开时,把 raw_content 截到预算内:

# utils.py:109-118 —— 真实源码(节选)
char_limit = max_tokens_per_source * CHARS_PER_TOKEN # 1000 * 4 = 4000 字符
raw_content = source.get("raw_content", "")
if raw_content is None:
raw_content = ""
if len(raw_content) > char_limit:
raw_content = raw_content[:char_limit] + "... [truncated]"

这里用了一个极粗的 token 估算:CHARS_PER_TOKEN = 4(utils.py:14),即「4 字符 ≈ 1 token」,配 MAX_TOKENS_PER_SOURCE = 1000(graph.py:41)得出每条来源上限约 4000 字符。不精确,但足够防止把整篇长网页灌爆本地模型的上下文。

4.4 抓全文:HTML → Markdown

fetch_full_page 开,DuckDuckGo/SearXNG 会对每条 URL 调 fetch_raw_content(utils.py:141-162):用 httpx10 秒超时拉网页,再用 markdownify 把 HTML 转成 Markdown。任何异常都吞掉、返回 None(utils.py:160-162),所以单个网页抓失败不会拖垮整轮。

一个配置矛盾值得留意: configuration.py:37-41fetch_full_page 默认 True,但 README.md:80 说它默认 false。以代码为准,默认是 True

4.5 两种「来源」文本,别混淆

项目里有两个生成来源文本的函数,用途不同:

函数输出给谁看位置
deduplicate_and_format_sources详细的 Source:/URL:/content喂给 LLM 做摘要(进 web_research_results)utils.py:55
format_sources简洁的 * title : url 列表sources_gathered,最终报告的引用清单utils.py:123

前者是「给模型读的料」,后者是「给人看的引用」。finalize_summary 去重的正是后者产出的那些 * title : url 行(见 02 章)。

代码地图

主题文件符号
DuckDuckGo 后端src/ollama_deep_researcher/utils.pyduckduckgo_search
SearXNG 后端src/ollama_deep_researcher/utils.pysearxng_search
Tavily 后端src/ollama_deep_researcher/utils.pytavily_search
Perplexity 后端 + 引用拆分src/ollama_deep_researcher/utils.pyperplexity_search
去重+格式化+token 截断src/ollama_deep_researcher/utils.pydeduplicate_and_format_sources
抓全文转 Markdownsrc/ollama_deep_researcher/utils.pyfetch_raw_content
人读引用清单src/ollama_deep_researcher/utils.pyformat_sources