这是这是我们《AI开发实践》系列的第15 篇我们继续学习Langchain。前一节中我们学习了基础的工具调用这一节我们来学习一种特殊的工具调用搜索引擎。在实际的项目中特别是toC的场景往往需要从网上实时搜索一些最新的信息作为源料提供给LLM进行分析因此我们必须要了解一下在LLM中如何使用搜索引擎。 在本示例中我们的目标是根据用户的购物诉求推荐一款最佳口碑的商品。工作的主流程是用户需求LLM调用搜索引擎搜索商品口碑信息LLM分析决策出唯一款最佳口碑商品最终输出 JSON在口碑推荐商品的过程中LLM会根据用户的需求决定是否要调用搜索工具。如果需要调用搜索工具则自动生成搜索关键词然后使用tavily_search作为搜索引擎负责在线检索商品口碑信息再将搜索得到的商品口碑信息作为决策依据根据评分规则计算出来一款最佳口碑商品。一、Langchain Tavily介绍在这个示例中我们会以搜索工具Tavily为例 。 Tavily是一款专为人工智能应用打造的网络搜索 API与传统搜索引擎返回链接不同Tavily 会返回带有上下文的简短相关摘要。它会在网络上搜索最相关的内容并将其整理为清晰、结构化的摘要返回供大语言模型直接使用。Tavily 可以借助 LangChain 集成来使用相关参考文档如下https://docs.langchain.com/oss/python/integrations/tools/tavily_searchhttps://docs.tavily.com/documentation/integrations/langchainTavily支持高级筛选、主题定位和结果控制具体的参数包括max_results(optional, int)要返回的最大搜索结果数量。默认值为5。topic(optional, str): 搜索的类别。可以是 “general”, “news”, or “finance”默认是 “general”。include_answer(optional, bool): 是否在结果中包含对原始查询的回答当为True时 Tavily 会基于返回的所有搜索结果自动生成一段针对你查询的自然语言回答并把它放进响应里。默认值为 False。include_raw_content(optional, bool): 是否返回每个搜索结果的清理后和解析后的完整 HTML拿到单页的全文内容后可以用于深度解析 / 二次处理。默认值为 False。include_images(optional, bool): 在响应中包含与查询相关的图片列表。默认值为 False。include_image_descriptions(optional, bool): 为每张图像包含描述性文本。默认值为 False。search_depth(optional, str): 搜索的深度可选值为“basic”基础或“advanced”高级。默认值为“basic”基础。time_range(optional, str): 从当前日期发布日期起向后筛选结果的时间范围——可选“day”最近1天、“week”最近1周、“month”最近1月或“year”最近1年。默认为无。start_date(optional, str): 将返回指定开始日期发布日期之后的所有结果。必须按照 YYYY-MM-DD 格式填写。默认值为无。end_date(optional, str): 将返回指定结束日期之前的所有结果。需按 YYYY-MM-DD 格式填写默认值为无。include_domains(optional, List[str]): 需特别包含的域名列表。最多300个域名。默认值为None。exclude_domains(optional, List[str]): 需要明确排除的域名列表。最多支持150个域名。默认值为None。include_usage(optional, bool): 是否在响应中包含帐号额度使用信息计费。默认值为 False。在我们的示例中 我们将其初始化为tavily_toolTavilySearch(search_depthfast,time_rangemonth,include_domains[zhihu.com,# 知乎xiaohongshu.com# 小红书],max_results3,include_raw_contentTrue,)二、tavily搜索最佳实践结合我们的开发实例 以及Tavily的最佳实践指导(https://docs.tavily.com/documentation/best-practices/best-practices-search)。 我们讲一下在本次实例中的优秀实践1. 优化搜索关键词1) 保持查询简洁,单个查询控制在 400 字符以内。在定义搜索工具hybrid_web_search时强制query字段必须满足规则“max_length400”tool(web_search_buying_info,description...)asyncdefhybrid_web_search(query:Annotated[str,Field(max_length400)])-str:...Langchain 在自动生成一段工具描述时塞进给 LLM 的 Prompt 里类似tools:[{type:function,function:{name:web_search_buying_info,description:Web搜索工具用于从互联网上搜索与购物相关的信息。\n当需要搜索商品信息时调用该工具。\n该工具的输入必须是“与用户问题相关”且“与口碑/评测/推荐类相关”的搜索词\n输出是搜索到的口碑/评测/推荐内容\n针对于一个用户问题该工具只能调用一次,parameters:{properties:{query:{maxLength:400,type:string}},required:[query],type:object}}}]2) 将复杂问题拆分为多个针对性强的子查询不要一次性扔给 Tavily 一个大问题。先用路由器 Agent 或 Planner 把问题拆成多个针对性强的子查询再并行调用 Tavily使用 asyncio.gather能显著提升相关性和降低成本。在本示例中通过定义一个带参的拆分提示词使用程序式提示词框架根据业务要求保证业务相关性和维度正交覆盖### Role角色 - 你是用户的购物顾问和市场情报分析师 - 你具备丰富的商品信息搜索经验和商品口碑分析能力 - 你始终以用户预算和实际使用需求为核心进行分析 ### Task任务 你的核心任务是将用户的购物问题拆分成独立、具体、高质量的子查询 ### Objective目标 - 精准获取用户需求对应的口碑/评测/推荐/榜单/用户体验类商品信息 - 为后续口碑筛选、计分、最优商品选择提供合规搜索数据 ### Schema (数据规范) #### Output Format输出格式 请严格按照以下 JSON 格式输出不要添加任何解释性文字 {{ sub_queries: [ 子查询1, 子查询2, 子查询3 ] }} ### Step Template (步骤模板) 请严格按照以下固定步骤进行运算每一步都根据前序的输出作为输入形成闭环依赖 Step 1. 需求解析提取语义 以用户的原始需求描述为输入提取用户需求中的 “商品类型、核心要求、使用场景”输出结构化需求要素列表。 Step 2. 维度规划语义映射 以上一步输出的结构化需求要素列表为输入确定口碑类信息核心维度 “口碑推荐、评测榜单、用户好评、热销首选、品牌口碑”输出确定的核心维度列表 Step 3. 子查询规划语义拆解 以上一步输出的核心维度列表和需求要素为输入针对核心维度构造口碑/评测/推荐/榜单/热销/用户体验类商品信息子查询无重复无交叉输出初步子查询列表 如输出子查询中有年份/公司名称/产品名称/产品型号等专有名称必须对专有名称使用双引号。 Step 4. 质量检查与优化语义校验 以上一步输出的初步子查询列表为输入检查每个子查询是否具体清晰、无重复、覆盖主要维度符合工具调用要求无无关信息 ### Constraints约束要求 - 子查询仅聚焦“口碑、评测、推荐、榜单、用户好评、首选、热销、TOP”等口碑类关键词 - 禁止构造价格、配置、促销、参数类非口碑搜索意图 - 必须覆盖预算、性能、用途、品牌对比、用户反馈、时效性核心维度。 - 仅按本规则执行不调用外部知识。 - 子查询数量建议 1~3 个。 - 每个子查询中的关键词数量不超过4个 - 如果不是重要专有名词严禁使用双引号精确匹配。 ### Example示例 用户需求推荐一款桌面静音小风扇 Step 1.需求解析提取语义 以用户的原始需求描述为输入提取用户需求中的 “商品类型、核心要求、使用场景”输出结构化需求要素列表商品类型 桌面小风扇核心要求 静音使用场景 桌面办公 / 家用。 Step 2.维度规划语义映射 以上一步输出的结构化需求要素列表为输入确定口碑类信息核心维度 “口碑推荐、评测榜单、用户好评、热销首选、品牌口碑”输出确定的核心维度列表口碑推荐、评测榜单、用户好评、热销首选、品牌口碑。 Step 3.子查询规划语义拆解 以上一步输出的核心维度列表和需求要素为输入针对核心维度构造口碑/评测/推荐/榜单/热销/用户体验类子查询无重复无交叉输出初步子查询列表桌面静音小风扇 口碑推荐、桌面静音小风扇 评测榜单、桌面静音小风扇 用户好评、桌面静音小风扇 首选款、桌面静音小风扇 品牌口碑排行。 Step 4.质量检查与优化语义校验 以上一步输出的初步子查询列表为输入检查每个子查询是否具体清晰、无重复、覆盖主要维度符合工具调用要求无无关信息所有子查询具体清晰、无重复、覆盖核心维度符合工具调用要求无无关信息。 {{ sub_queries: [ 桌面静音小风扇 口碑推荐, 桌面静音小风扇 评测榜单, 桌面静音小风扇 用户好评, ] }} ### Evaluation (自检规则) 输出最终子查询列表前必须严格执行以下自检全部通过才可输出 - 是否仅包含前述的口碑类关键词 - 是否以JSON结构化输出 - 如果关键中包含年份必须是今年 请严格按上述 SPC-E 框架处理以下用户需求 用户需求{question} 通过langchain来定义一个工作链调用LLM完成拆分#使用ChatPromtTemplate定义一个带参的提示词用于拆分搜索成多个子关键词搜索websearch_planner_promptChatPromptTemplate.from_template(open(resources/prompt/websearch_buying.txt,r,encodingutf-8).read())#LangChain 的「流水线写法」把「提示词模板 → 大模型 → JSON 解析」串成一条自动执行的链 执行关键词拆分planner_chainwebsearch_planner_prompt|model|JsonOutputParser()tool(web_search_buying_info,descriptionWeb搜索工具用于从互联网上搜索与购物相关的信息)asyncdefhybrid_web_search(query:Annotated[str,Field(max_length400)])-str:#调用lagnchain的规划链将LLM的原始搜索请求拆分搜索请求成子关键词planawaitplanner_chain.ainvoke({question:query})#获取返回的关键词列表比如 {鱼缸增氧泵 用户好评 静音, 鱼缸打氧器 静音 评测 2026, 静音鱼缸氧气泵 口碑推荐}sub_queries:List[str]plan.get(sub_queries,[query])...#获取有效的valid_unique_queries...#分别调用hybrid_search_one执行查询all_results[]forqinvalid_unique_queries:try:resawaithybrid_search_one(q)all_results.append(res)exceptExceptionase:print(f搜索{q}失败{e})all_results.append([])print(fhybrid_web_search, engine {SEARCH_ENGINE}, all_results {all_results})比如针对于用户问题我想买个鱼缸打氧器静音要好。 LLM原始的搜索请求是鱼缸打氧器 静音 推荐 评测 口碑 哪个好 2026。此时本地工具hybrid_web_search会首先使用planner_chain根据提示词websearch_buying.txt 来拆分为多个子提示词{鱼缸增氧泵 用户好评 静音, 鱼缸打氧器 静音 评测 2026, 静音鱼缸氧气泵 口碑推荐}; 然后每个提示词会单独的调用tavily完成搜索(如下hybrid_search_one())# 执行搜索asyncdefhybrid_search_one(query:str)-List[Dict]:results[]seen_urlsset()print(fstart hybrid_search_one, query {query})try:resawaittavily_tool.ainvoke({query:query})ifisinstance(res,dict)andresultsinres:foriteminres[results]:urlitem.get(url)ifurlandurlnotinseen_urls:results.append(item)seen_urls.add(url)exceptExceptionase:print(fTavily 搜索失败{e})returnresults2. 合理设置搜索范围1) 根据业务诉求选择合适的搜索深度在Tavily Search返回的结构体中results: [ { url: ..., title: ..., content: ..., score: 0.86767644, raw_content: ... } ],‘content’ 中可能是Content或者Chunks,Type 类型Description 描述Content基于自然语言处理的页面摘要NLP-based summary of the page, providing general contextChunks根据与搜索查询的相关性重新排序的简短片段Short snippets reranked by relevance to your search query具体取决于search_depth参数的配置ultra-fastLowest 最低Lower 更低Content TypefastLow 低Good 好的ChunksDepth 深度Latency 延迟Relevance 相关性ContentbasicMedium 中等High 高ContentadvancedHigher 更高Highest 最高Chunks在我们的最佳口碑商品推荐的例子中我们做的是精准的业务筛选自己来挑选信息再提交给LLM做分析决策不使用搜索引擎给出的摘要或片段信息。因此通过设置 search_depth“fast” 参数实现更快的搜索只有需要深度分析时才用 “advanced”成本约 2 倍2控制信息筛选范围通过配置time_range、topic、include_domains、exclude_domains等缩小搜索范围减少噪音。设置time_rangemonth,查询最近1个月内的信息设置topicgeneral,查询通用信息其它的 “news”, “finance” 更不合适设置include_domains, 限定只在第三方评论平台内进行搜索include_domains[zhihu.com,# 知乎xiaohongshu.com# 小红书]除以上参数外tavily原生是支持country,Exact Match 当前langchain暂未支持。country, 只支持搜索一个国家的内容比如 cnExact Match , 如果为True, 对于带引号的关键词进行精确匹配 , 同时其它的非带引号关键词仍执行语义搜索 如果为False默认, 则全部为语义搜索。对于年份/公司名称/产品名称/产品型号等专有名称建议使用双引号。3. 控制响应内容1) 控制返回结果数量搜索引擎返回的结果太多往往相关性差的结果就越多。本程序中max_results3每次搜索搜索引擎只返回3个结果。默认 5最多不要超过 10。2) 本地自主提取关键信息通过设置include_raw_contentTrue 获取原始内容, 进而程序自己从原文中摘取关键信息再提供给LLM进行汇总。目的是保证喂给LLM的信息是业务有效信息同时精确控制Token消耗。注意对于搜索结果避免使用硬截断因为很容易把关键信息给丢掉了导致后续LLM无有效输入信息。defextract_key_sentences(text:str,max_total_len:int1800)-str:ifnottext:return# 【必须保留】关键词REQUIRED_SIGNALS{推荐,口碑,评测,榜单,首选,TOP1,第一名,最推荐,口碑最佳,闭眼入,公认好用,榜单第一,无限回购,口碑之王,差评少,行业标杆,博主推荐,款,品牌,型号,商品,产品,这款,他家}# 1. 把文本按【完整句子】切割支持中文标点sentencesre.split(r(。|||!\?|\n),text)sentences[s.strip()forsinsentencesifs.strip()]# 2. 只保留包含关键词的句子keep[]forsentenceinsentences:# 命中任意一个关键词即保留ifany(keywordinsentenceforkeywordinREQUIRED_SIGNALS):keep.append(sentence)# 3. 拼接成干净文本result .join(keep)# 4. 防止极端超长iflen(result)max_total_len:resultresult[:max_total_len].rsplit( ,1)[0]...returnresult也可以结合Extract API在搜索得到 URL 后再调用 Extract 获取结构化全文内容提高上下文质量。参考https://docs.tavily.com/documentation/best-practices/best-practices-extract#2-two-step-process-search-then-extract4. 使用元数据Field 字段Use case 用例scoreFilter/rank by relevance score 按相关性分数筛选/排序titleKeyword filtering on headlines 标题关键词筛选contentQuick relevance check 快速相关性检查raw_contentDeep analysis and regex extraction 深度分析与正则表达式提取1Score-based filtering 基于分数的筛选仅取相关度分数score0.7的进入结果进行排序raw_contentitem.get(raw_content,)if(urlandurlnotinseenandscoreisnotNone# 防止score为空andscore0.7# 分数过滤andis_valid_for_product_ranking(raw_content)):merged.append(item)seen.add(url)print(furl{url})2按Score和Content长度进行排序根据score和内容长度进行优先级排序#排序先按原始搜索分再按内容长度 merged.sort( keylambda x: ( # 第一优先级Tavily 原始搜索分数越高越相关 x.get(score, 0.0), # 第二优先级内容越长越完整可选 len(x.get(content, )) ), reverseTrue # 从高到低 )5. 验证LLM返回结果对于搜索引擎或者LLM的返回结果一定要进行验证才能进入后续的程序处理流程。验证1搜索关键词拆分结果验证在程序化提示词中通过### Evaluation (自检规则)对输出结果进行自检比如### Evaluation (自检规则) 输出最终子查询列表前必须严格执行以下自检全部通过才可输出 - 是否仅包含前述的口碑类关键词 - 是否以JSON结构化输出 - 如果关键中包含年份必须是今年那么在程序中要基于同样的要求对于输出结果进行验证#校验单个生成的口碑类关键词是否合规defvalidate_single_sub_query(sub_query:str)-bool:# 合法口碑关键词集合查询效率更高ALLOWED{口碑,评测,推荐,榜单,用户好评,热销,首选,TOP}# 禁止关键词FORBIDDEN{价格,参数,优惠,多少钱}# 1. 校验输入类型为字符串ifnotisinstance(sub_query,str):returnFalse# 2. 去除首尾空格后不能为空stripped_querysub_query.strip()ifnotstripped_query:returnFalse# 3. 必须包含至少一个合法口碑词has_allowedany(kwinstripped_queryforkwinALLOWED)ifnothas_allowed:returnFalse# 4. 不能包含任何禁止词has_forbiddenany(kwinstripped_queryforkwinFORBIDDEN)ifhas_forbidden:returnFalse# 所有校验通过returnTrue验证2搜索结果业务有效性验证搜索引擎的返回结果是不确定的因此一定要用程序进行验证确保其能符合后续的业务要求。 比如在搜索产品相关的口碑信息后我们需要先验证一下是否包含相关的口碑关键词和商品关键词。如果没有后续LLM就无法利用其进行分析。#校验生成的搜索的内容是否合规# 校验单条 content 内容是否合规用于商品榜单/口碑# 传入单条 content 字符串# 返回True 合格False 不合格defis_valid_for_product_ranking(content:str)-bool:# 如果内容为空直接不合格ifnotcontentornotisinstance(content,str):returnFalse# 必须包含的强信号词REQUIRED_SIGNALS{推荐,口碑,评测,榜单,首选,TOP1,第一名,最推荐,口碑最佳,闭眼入,公认好用,榜单第一,无限回购,口碑之王,差评少,行业标杆,博主推荐}# 商品相关关键词PRODUCT_KEYWORDS{款,品牌,型号,商品,产品,这款,他家}# 检查是否包含有效信号has_signalany(kwincontentforkwinREQUIRED_SIGNALS)# 检查是否包含商品相关词has_productany(kwincontentforkwinPRODUCT_KEYWORDS)# 两个条件都满足才算合格returnhas_signalandhas_product6. 防止LLM反复调用工具在这个示例的验证过程中发现LLM经常会多次的调用工具进行搜索主要原因是工具的说明并未告知模型何时停止调用它。模型调用工具后如果得到的结果无法解答问题会判定该工具依然相关然后再次调用。我们需要在工具的使用手册中不仅要告知模型该工具的功能还要进行说明what a good input looks like 优质输入的样子what the output represents 输出代表的内容when the tool has given you enough to answer当工具为你提供了足够的信息来回答问题时when to give up and try a different tool何时放弃并尝试其他工具tool(web_search_buying_info,description Web搜索工具用于从互联网上搜索与购物相关的信息。 当需要搜索商品信息时调用该工具。 该工具的输入必须是“与用户问题相关”且“与口碑/评测/推荐类相关”的搜索词 输出是搜索到的口碑/评测/推荐内容 针对于一个用户问题该工具只能调用一次)asyncdefhybrid_web_search(query:Annotated[str,Field(max_length400)])-str:更多的方法可以参考https://dev.to/gabrielanhaia/why-your-langchain-agent-keeps-calling-the-same-tool-in-a-loop-and-how-to-stop-it-57gk7. 跟踪帐户消耗调测期内如果帐户有免费额度限制要注意观察帐户余量include_usageTrue配置include_usage 系统在每次调用会返回还有多少帐户额度8. 其它使用轻量/廉价模型的路由 Agent来判断是否需要搜索、是否拆分查询减少无效 Tavily 调用。搭建语义缓存Redis 向量相似度减少 LLM 决策带来的重复搜索成本。由 LLM 对搜索结果做相关性校验无效则直接终止搜索并兜底。接入全链路追踪工具关联 LLM 决策与 Tavily 调用实现行为可追溯。三、总结在这个示例中搜索引擎工具的调用与普通工具最大的区别在于搜索引擎的输入和输出都是不确定的但是我们却必要要保证”LLM-搜索引擎-程序“三者之间在业务逻辑上的一致性和连贯性。我们要通过提示词去引导确定性通过程序去控制确定性持续的把LLM和搜索引擎可能走弯的路给别回来。比如通过提示词要求搜索方法的输入是”该工具的输入必须是“与用户问题相关”且“与口碑/评测/推荐类相关”的搜索词输出是搜索到的口碑/评测/推荐内容“ 通过程序的validate_single_sub_query()来验证拆分后的每个搜索关键词通过is_valid_for_product_ranking()来验证搜索引擎返回结果的业务有效性。