技术博客
在 RAG 里接入百度搜索 API:references 从过滤到注入的三步设计

在 RAG 里接入百度搜索 API:references 从过滤到注入的三步设计

作者: 万维易源
2026-09-15
百度搜索RAGAI Agent上下文注入
# 在 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)