ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

【大语言模型应用】用 DuckDuckGo 与 Tavily 给 LLM 搭一条可验证的搜索链路

【大语言模型应用】用 DuckDuckGo 与 Tavily 给 LLM 搭一条可验证的搜索链路 1. 为什么单靠模型自己答不准联网检索的真实痛点大语言模型联网检索这件事很多人第一反应是「给模型加个搜索工具不就行了」。真上手才发现问题不在「能不能搜」而在「搜回来的东西能不能用」。我拿同一个问题分别问过纯模型和带检索的模型纯模型给的答案里时间、版本号、API 参数经常是编的而且编得很像真的你不对着官方文档根本看不出来。核心检索词先摆在这大语言模型联网检索本质是让模型在生成前先拿到外部证据再把证据塞进上下文。DuckDuckGo 和 Tavily 是两条典型路线——前者是面向人的隐私搜索引擎后者是面向 AI 的搜索 API。把它们接进 LLM能解决三类具体问题。第一类是时效性问题。模型的训练数据有截止日期问它「某个库最新版本怎么配置」它只能靠记忆猜。检索链路能在提问瞬间把最新文档抓回来。第二类是幻觉问题。模型不知道答案时会硬编。如果上下文里已经有真实网页摘要它就更倾向于基于证据回答而不是凭空造。第三类是可控性问题。纯模型回答你没法追溯来源检索链路每条结果都带 URL你能点进去核对这对技术选型、参数确认特别重要。但这里有个坑不是所有搜索源都适合喂给模型。DuckDuckGo 返回的是给人看的网页列表标题加摘要格式松散Tavily 返回的是结构化 JSON带 title、url、content、score甚至能直接给 answer 字段。你把 DuckDuckGo 的原始 HTML 结果直接塞进 prompttoken 浪费严重模型还得自己从一堆导航栏、广告文案里挑重点。所以「双搜索源」的价值不是二选一而是按场景分工需要隐私、免费、覆盖广的时候走 DuckDuckGo需要高质量、低 token、带排序的时候走 Tavily。我实测下来一个稳定的检索链路要满足四个条件搜索源可切换、结果格式统一、注入上下文前做裁剪、失败能降级。下面就从接入配置开始一步步把这条链路搭起来。2. TaoToken 前置统一 Base URL 与 Key 的接入准备在写搜索代码之前得先把模型调用这一端固定下来。因为检索链路最终要把搜索结果注入 LLM 上下文如果模型接口本身东一个 Key 西一个地址调试检索质量时你分不清是搜索的问题还是模型的问题。我的做法是统一走一个兼容 OpenAI 协议的入口这样搜索源可以换模型调用层不动。TaoToken 提供的就是这样一个入口。官网地址是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 根地址是 https://taotoken.net/api 。注意 API 地址不带 UTM 参数配置里直接写这个就行。你需要准备两样东西一个 API Key一个模型 ID。Key 在控制台的 API Keys 页面生成地址是 https://taotoken.net/console/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite 。生成后复制保存后面所有配置都用它。模型 ID 这块检索链路里我一般用通用对话模型做「证据整合」这一步把搜索回来的多条结果压缩成一段干净上下文。具体模型名以控制台或文档里列出的为准文档入口在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 。这里要强调一个概念Base URL 和 Key 是「模型侧」的配置DuckDuckGo 和 Tavily 是「搜索侧」的配置两者独立。很多人第一次搭检索链路会把它们混在一起结果报 401 时不知道是模型 Key 错了还是搜索 Key 错了。分开管理出问题好定位。如果你用的是 Claude Code 这类编码工具它内部也要配 Base URL、Key、Model ID 三件套逻辑是一样的。TaoToken 的 coding-plan 页面 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite 里有针对长期编码场景的说明检索链路调试阶段用按量调用就够了。环境变量建议这样组织把模型侧和搜索侧分开# 模型侧TaoToken 统一入口 export TAOTOKEN_BASE_URLhttps://taotoken.net/api export TAOTOKEN_API_KEYsk-你的key # 搜索侧 export TAVILY_API_KEYtvly-你的key # DuckDuckGo 走公开接口无需 Key这样后面写代码时模型调用只认 TAOTOKEN_ 前缀搜索调用只认 TAVILY_ 前缀职责清晰。下一步进入可复制配置。3. 可复制配置双搜索源 模型调用的完整片段这一节给可直接粘贴的配置和代码。先看模型侧的 settings 片段如果你用支持 OpenAI 协议的 SDK配置长这样{ base_url: https://taotoken.net/api, api_key: sk-你的key, model: 控制台列出的模型ID, temperature: 0.2 }temperature 调低是因为检索整合任务要的是忠实于证据不是发挥创意。搜索侧Tavily 用官方 SDK 最省事。先装依赖pip install tavily-python openai duckduckgo-searchDuckDuckGo 这里用 duckduckgo-search 这个库它封装了公开搜索接口不需要 Key。Tavily 需要 Key在它官网注册后拿到。下面是一个把两个搜索源统一成同一返回结构的封装。关键点是不管底层是哪个源上层拿到的都是[{title, url, content}]列表这样注入上下文时格式一致。import os from tavily import TavilyClient from duckduckgo_search import DDGS def search_tavily(query, max_results5): client TavilyClient(api_keyos.environ[TAVILY_API_KEY]) resp client.search(query, max_resultsmax_results, search_depthadvanced) return [ {title: r[title], url: r[url], content: r[content]} for r in resp.get(results, []) ] def search_ddg(query, max_results5): results [] with DDGS() as ddgs: for r in ddgs.text(query, max_resultsmax_results): results.append({ title: r.get(title, ), url: r.get(href, ), content: r.get(body, ) }) return results def search(query, sourcetavily, max_results5): if source tavily: return search_tavily(query, max_results) elif source ddg: return search_ddg(query, max_results) raise ValueError(funknown source: {source})注意 Tavily 的search_depthadvanced会做多步检索返回质量更高但耗时更长调试阶段可以先用默认的 basic确认链路通了再切 advanced。接下来是把搜索结果注入模型上下文的函数。这里有个裁剪技巧每条结果只保留前 500 字符避免单条超长网页把上下文撑爆。from openai import OpenAI client OpenAI( base_urlos.environ[TAOTOKEN_BASE_URL], api_keyos.environ[TAOTOKEN_API_KEY], ) def build_context(results, per_item_limit500): blocks [] for i, r in enumerate(results, 1): snippet r[content][:per_item_limit] blocks.append(f[{i}] {r[title]}\nURL: {r[url]}\n{snippet}) return \n\n.join(blocks) def ask_with_search(query, sourcetavily): results search(query, sourcesource) context build_context(results) prompt f基于以下检索结果回答问题只使用结果中的信息无法回答时明确说明。 检索结果 {context} 问题{query} resp client.chat.completions.create( model控制台列出的模型ID, messages[{role: user, content: prompt}], temperature0.2, ) return resp.choices[0].message.content, results这段代码就是整条链路的核心。ask_with_search返回两个值模型答案和原始检索结果后者用于核对来源。到这里配置部分完成下一节验证它到底跑不跑得通。4. 验证请求同一问题对比两源返回差异配置写完不验证等于没写。这一节用同一个问题分别跑 Tavily 和 DuckDuckGo看返回差异同时确认模型整合是否正常。选一个有时效性、又能体现检索质量的问题比如「某个 Python 库最新版本的安装方式」。先跑 Tavilyanswer, results ask_with_search(Python requests 库最新版本安装方式, sourcetavily) print( Tavily 答案 ) print(answer) print(\n 来源 ) for r in results: print(r[url])再跑 DuckDuckGoanswer, results ask_with_search(Python requests 库最新版本安装方式, sourceddg) print( DDG 答案 ) print(answer) print(\n 来源 ) for r in results: print(r[url])实测下来两者差异通常体现在三个维度。第一是结果结构。Tavily 返回的 content 字段已经是清洗过的正文摘要直接可用DuckDuckGo 的 body 字段有时是网页 meta 描述信息量偏少偶尔混入导航文案。所以同样 5 条结果Tavily 注入后模型能用的信息更多。第二是排序质量。Tavily 的 score 排序对技术问题更友好官方文档、权威博客通常排前面DuckDuckGo 的排序更接近通用网页搜索有时会把问答站、聚合站排前面。第三是延迟和成本。DuckDuckGo 免费但偶尔限流连续快速请求可能返回空Tavily 有配额但稳定advanced 模式单次耗时明显更长。验证时还要看模型整合这一步。如果模型答案里出现了检索结果中没有的细节说明它在编这时候要么降低 temperature要么在 prompt 里加强「只使用结果中的信息」的约束。如果模型答案正确引用了来源编号说明注入格式没问题。一个更严格的验证动作故意问一个检索结果里没有的问题看模型是否老实说「无法回答」。如果它硬答说明约束不够需要把 prompt 里的兜底句写得更硬。answer, _ ask_with_search(一个检索结果里绝对没有的冷门问题, sourcetavily) print(answer) # 期望看到「无法从检索结果中回答」之类这一步过了链路才算真正可用。下一节处理跑不通的情况。5. 常见报错排查401、local proxy failed、reading choices、OAuth搭链路时踩的坑基本集中在四类报错逐个说。401 Unauthorized。这个最常见分两种。一种是模型侧 401说明 TAOTOKEN_API_KEY 错了或没生效。检查环境变量是否真的导出成功echo $TAOTOKEN_API_KEY看有没有值。另一种是 Tavily 侧 401说明 TAVILY_API_KEY 无效。注意两个 Key 前缀不同别贴混。如果用的是 Claude Code 或 Cline 这类工具401 往往是因为 Base URL 末尾多了斜杠或少了/api正确写法是https://taotoken.net/api不要写成https://taotoken.net/api/。local proxy failed。这个报错通常出现在工具类客户端里意思是客户端尝试走本地代理但连不上。排查方向检查客户端配置里是否残留了代理设置把代理项清空确认 Base URL 是直连地址而不是某个本地端口。如果你在 settings 里配了http_proxy之类的环境变量先 unset 掉再试。reading choices 相关报错。典型信息是NoneType object is not subscriptable或reading choices意思是接口返回体里没有 choices 字段。原因一般是模型 ID 写错了服务端返回了错误对象而不是正常响应或者请求根本没到模型服务被中间层拦截返回了 HTML。排查时先把原始响应打出来resp client.chat.completions.create(...) print(resp) # 看结构如果返回的是错误 JSON里面通常有 message 字段说明原因。模型 ID 一定要和控制台列出的完全一致大小写、连字符都不能错。OAuth 相关报错。如果你用 Claude Code 这类工具它可能默认走 OAuth 登录流程而你要用的是 API Key 模式。这时候需要在配置里显式指定用 Key 认证而不是让它弹浏览器登录。Claude Code 的接入配置里Base URL、Key、Model ID 三件套要写全缺一个就可能回退到 OAuth 流程然后失败。具体配置参考文档 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 。还有一个隐蔽的坑DuckDuckGo 连续请求被限流时不会报错而是返回空列表。这时候模型会基于空上下文硬答。所以代码里要加判断结果为空时直接返回「未检索到结果」不要硬塞给模型。results search(query, sourceddg) if not results: return 未检索到结果请稍后重试或切换搜索源, []排障的核心思路是分层先确认模型侧通不通单独发一条不带检索的消息再确认搜索侧通不通单独打印搜索结果最后才看整合环节。分层定位比盯着一个报错猜快得多。6. 检索链路的长期用法与接入入口链路跑通之后日常使用还有几个优化点。第一是缓存同一个问题短时间内重复检索没意义可以在 search 函数外面套一层简单的内存缓存key 用 querysource。第二是降级Tavily 配额用完或超时时自动切 DuckDuckGo保证链路不断。第三是结果去重两个源可能有重叠 URL注入前按 url 去重能省 token。def search_with_fallback(query, max_results5): try: results search(query, sourcetavily, max_resultsmax_results) if results: return results, tavily except Exception: pass results search(query, sourceddg, max_resultsmax_results) return results, ddg如果你要把这条链路用在长期编码或 Agent 场景模型调用量会上去按量计费不如包月划算可以看 coding-plan https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite 。只是想快速验证某个模型整合检索结果的效果用模型对话页 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewrite 手动贴上下文试几次比写代码快。Key 管理和配额查看在控制台 https://taotoken.net/console/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite 接入细节和参数说明在文档 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 。最后说个实际经验检索链路的质量瓶颈往往不在模型而在你给它的证据干不干净。与其反复调 prompt不如先把搜索结果裁剪和去重做好模型自然答得准。
返回列表