在 RAG 里接入百度搜索 API:references 从过滤到注入的三步设计
# 在 RAG 里接入百度搜索 API:references 从过滤到注入的三步设计
> 接口:百度搜索(apiCode=3351,接入点 3351-1)· 官方自营 · 按次计费
> 请求方式:POST / GET · 适用人群:做 AI 应用、需要实时检索的开发者 · 阅读时间:约 9 分钟
> **最后实测核对:2026-09-15(104 条真实结果样本)**
用户问"最近有什么大模型发布",你的模型答不上来——它的知识截止在训练那天。你要做的是在回答之前先搜一次,把搜到的内容塞进它的上下文。
百度搜索 API(apiCode=3351)返回的 `references` 数组天生适合干这个:每条都带标题、正文片段、发布时间和来源。但这批数据不能原样丢给模型,得先过三道处理。这篇讲这三道怎么设计。
## 核心要点
- 检索、清洗、注入三段要分开写,别揉在一个函数里。
- `content` 字段本身就有内容,多数情况不用二次抓取;空了再退回 `snippet`。
- 用 `id` 生成引用角标、用 `url` 去重,是让模型"说得有依据"的最小成本做法。
## 为什么不能直接拼
实测 2026-09-15 的一次调用(`query=昆明天气`)返回 10 条结果,其中第一条的 `content` 就有几百字正文片段。十条拼起来轻松超过三千字,而且这十条里通常混着重复内容、无关站点、没有站点名的条目。
直接拼进 prompt 有三个后果:token 白烧;模型被无关内容带偏;引用来源无法回溯,用户看到的是"我搜到过"而不是"据某某报道"。
所以中间必须有一层清洗。
## 第一步:把用户问题转成检索词
这个接口的 `query` 上限是 **72 个字符**(一个汉字算两个字符,即 36 个汉字),超长部分只取前 72 字符检索。实测验证过:用 59 个汉字的长句调用,和把它截到前 36 个汉字再调用,返回的 10 条 `url` 完全一致。也就是说,多出来的部分根本没参与检索。
用户问的是"能不能帮我看看这周有什么新的大模型发布",直接丢进去只有前 36 个字有效,剩下的浪费了。建议在检索前做一次关键词提取:
```python
def build_query(user_text: str, llm) -> str:
"""把自然语言问题压成检索词。用你自己的模型或规则都行。"""
prompt = (
"把下面的问题压缩成搜索引擎查询词,只输出查询词,"
"不要标点,不要解释,控制在 20 个汉字以内:\n" + user_text
)
return llm(prompt).strip()[:72]
```
不想引入模型调用就用规则:抽名词、去停用词、拼成短语。重点是**卡在 72 字符以内**,别让接口替你截断。
如果问的是"最新""最近"这类时效问题,顺手把 `search_recency_filter` 设上,否则可能搜到几年前的文章。四个可选档位是 `week` / `month` / `semiyear` / `year`,实测结果日期分布见[时间过滤那篇](https://www.showapi.com/guides/baidu-search-api-recency-filter-3351)。
## 第二步:清洗与筛选
清洗要解决三件事:去重、丢弃无来源条目、按质量排序。
去重键用 `url`,不要用 `id`——`id` 是一次响应内的递增编号,换查询就重置。
实测有个必须处理的坑:**`website` 会为空字符串,也会返回字面量"无"**。65 条去重结果里有 17 条是这样(2026-09-15 统计)。不判空的话,你的信源列表里会多出一个叫"无"的站点。
评分字段怎么用?实测 104 条样本里,`rerank_score` 大量等于 1,几乎没有区分度;`authority_score` 只出现过 0.5 和 1 两个值,反而更有区分度。
```python
def clean(refs: list) -> list:
seen, out = set(), []
for r in refs:
url = (r.get("url") or "").strip()
if not url or url in seen:
continue
seen.add(url)
text = (r.get("content") or "").strip() or (r.get("snippet") or "").strip()
if not text: # 正文和摘要都空,进上下文没意义
continue
site = r.get("website")
if site in (None, "", "无"):
site = "未知站点"
out.append({
"id": r.get("id"),
"title": (r.get("title") or "").strip(),
"url": url,
"site": site,
"date": r.get("date") or "",
"text": text,
"authority": r.get("authority_score") or 0,
})
# 权威性优先,其次新→旧
out.sort(key=lambda x: (x["authority"], x["date"]), reverse=True)
return out
```
排序里我把 `authority_score` 放主键、`date` 放次键。这是针对"要不要信这条"的目标定的,你可以换成时间优先——取决于你的场景更怕过时还是更怕不准。
## 第三步:拼上下文并保留引用
注入模板的关键是给每条编号,并要求模型引用编号。这样答案里能出现 `[1]` `[2]`,前端就能渲染成可点击的来源。
```python
CONTEXT_TEMPLATE = """以下是检索到的实时资料,回答时请引用编号:
{blocks}
要求:
1. 只依据上述资料回答,资料里没有的内容明确说不确定。
2. 每个事实性结论后面标注来源编号,如 [1]。
"""
def build_context(items: list, max_chars: int = 3000) -> tuple[str, list]:
blocks, used, total = [], [], 0
for i, it in enumerate(items, 1):
block = (f"[{i}] {it['title']}\n"
f"来源:{it['site']}({it['date']})\n"
f"{it['text']}\n")
if total + len(block) > max_chars:
break
blocks.append(block)
used.append({**it, "cite_index": i})
total += len(block)
return CONTEXT_TEMPLATE.format(blocks="\n".join(blocks)), used
```
`max_chars` 就是你的预算阀门。实测单条 `content` 最多 2000 字,所以 `max_chars=3000` 大约能装下 2 到 8 条,具体看每条长度。用户问的东西越具体,越该少而精。
返回的 `used` 列表要留着,前端拿它把 `[1]` 映射成真实链接:
```python
def render_citations(answer: str, used: list) -> str:
for item in used:
idx = item["cite_index"]
answer = answer.replace(
f"[{idx}]",
f'<a href="{item["url"]}" target="_blank" rel="noopener">[{idx}] {item["site"]}</a>',
)
return answer
```
## 完整链路
```python
import requests
def search(query: str, appkey: str, recency: str | None = None, count: str = "10"):
data = {"query": query, "count": count}
if recency:
data["search_recency_filter"] = recency
resp = requests.post(
"https://route.showapi.com/3351-1",
params={"appKey": appkey},
data=data,
timeout=10,
)
resp.raise_for_status()
result = resp.json()
if result.get("showapi_res_code") != 0:
return []
return (result.get("showapi_res_body") or {}).get("references") or []
def rag_retrieve(user_text: str, appkey: str, llm) -> tuple[str, list]:
refs = search(build_query(user_text, llm), appkey, recency="week")
items = clean(refs)
if not items:
return "", []
return build_context(items)
```
注意 `search()` 里错误分支返回空列表而不是抛异常。RAG 里检索失败降级成"没搜到资料"比让整个回答崩掉更合理,错误细节记日志就行。
## 进阶与边界
**一次调用最多 50 条,没有翻页。** 想要更多候选得换关键词多调几次,或者从不同角度构造 2 到 3 个 `query` 再合并去重。合并时仍按 `url` 去重。
**过滤会把条数打下来,不会补齐。** 实测固定 `query=大模型` 加 `search_recency_filter=week`,返回 10 条;再加 `block_domain_filter=baijiahao.baidu.com`,只剩 1 条。所以预算要按"过滤后可能只剩个位数"来估,别假设 `count=10` 就一定拿到 10 条。
**`content` 是片段不是全文。** 需要完整原文的场景,还得用 `url` 二次抓取。目标是"让模型知道有这回事",片段够用。
**模型引用编号不是必然行为。** prompt 里要求了引用,模型仍可能漏标。前端渲染时对没有角标的句子做兜底提示,别装作有来源。
**时间粒度别做太细。** 实测很多结果的 `date` 时分秒是 `00:00:00`,说明源页面只精确到日期。做"最近 3 小时"这类过滤会误伤。
## FAQ
**Q1:RAG 里该用 `content` 还是 `snippet`?**
优先 `content`,它最多有 2000 字原文片段。实测里它常为空值的场景不多,但为空时要退回 `snippet`,两个都空就丢弃这条。
**Q2:`rerank_score` 能用来筛相关性吗?**
区分度有限。实测 104 条里它大量等于 1,筛不出东西。`authority_score` 的取值分布更分散,更适合做质量门槛。
**Q3:10 条结果够用吗?**
看问题类型。事实型问题 3 到 5 条足够,需要多角度的综述型问题建议多轮检索合并。一次调用的上限是 50 条。
**Q4:用户问的问题很长,怎么处理?**
先压缩到 36 个汉字以内再提交。超长部分不会参与检索,实测已确认。
**Q5:检索没结果时怎么回答用户?**
把"没搜到"如实告诉用户,别让模型凭空补。接口返回空数组时走降级分支,用模型自身的知识回答并说明时效限制。
## 下一步阅读
- [百度搜索 API 域名过滤只需两个参数](https://www.showapi.com/guides/baidu-search-api-domain-filter-3351) —— 控制信源白名单和黑名单
- [百度搜索 API 返回字段:references 里 13 个字段逐个说清](https://www.showapi.com/guides/baidu-search-api-response-fields-3351) —— 清洗时每个字段的取值边界
- [百度搜索 API 计费与缓存:失败请求扣不扣费](https://www.showapi.com/guides/baidu-search-api-billing-cache-3351) —— 调用量上来了先看这篇
---
- **本系列共 8 篇**:查看[百度搜索 API 指南总目录](https://www.showapi.com/guides/baidu-search-guides-3351)