网页检索问答
模型只"知道"训练截止前的事,要回答新问题、查实时资料,就得让它先上网搜。本章实现一条完整链路:给智能体加 search_web(搜索)与 fetch_web(抓正文)两个工具,用标准库清洗网页正文,把检索结果喂给模型并要求给出来源 URL。
search_web:搜索工具
示例用维基百科的开放搜索接口当"搜索引擎"——稳定、免费、无需 key、返回 JSON。正式项目把它换成你买的搜索 API(如必应、博查等),函数签名不变:
# agent_demo/web_tools.py —— 第 14 章
import os
import requests
from urllib.parse import quote
SEARCH_API = os.environ.get("SEARCH_API",
"https://zh.wikipedia.org/w/api.php")
def search_web(query: str, top_k: int = 3) -> str:
"""网页搜索,返回标题 + 摘要 + 链接,供模型或后续抓取使用。"""
params = {"action": "query", "list": "search", "srsearch": query,
"srlimit": top_k, "format": "json", "utf8": 1}
r = requests.get(SEARCH_API, params=params, timeout=10)
r.raise_for_status()
hits = r.json()["query"]["search"]
if not hits:
return "没有搜到相关内容"
lines = []
for i, h in enumerate(hits, 1):
url = "https://zh.wikipedia.org/wiki/" + quote(h["title"])
lines.append(f"{i}. {h['title']}\n {h['snippet']}\n {url}")
return "\n".join(lines)
srlimit 限制条数、srsearch 是关键词;snippet 里可能残留 HTML 标签,正好用下面的清洗函数处理(示例中把清洗用在抓正文上,也可顺手套在摘要上)。
正文清洗:标准库 html.parser
抓到的网页是 HTML,直接喂给模型又费 token 又容易读偏。写一个只留正文的提取器(跳过 script/style,块级标签处换行):
# agent_demo/web_tools.py(续)—— 正文清洗
from html.parser import HTMLParser
class _Text(HTMLParser):
SKIP = {"script", "style", "nav", "header", "footer"}
def __init__(self):
super().__init__()
self.parts, self.skip = [], 0
def handle_starttag(self, tag, attrs):
if tag in self.SKIP:
self.skip += 1
if tag in ("p", "div", "li", "br", "h1", "h2", "h3"):
self.parts.append("\n")
def handle_endtag(self, tag):
if tag in self.SKIP and self.skip:
self.skip -= 1
def handle_data(self, data):
if not self.skip:
self.parts.append(data)
def clean_html(html: str, max_len: int = 2000) -> str:
"""把 HTML 变成可读纯文本,最多保留 max_len 字符。"""
p = _Text()
p.feed(html)
text = "".join(p.parts)
text = "\n".join(line.strip() for line in text.splitlines() if line.strip())
return text[:max_len]
fetch_web:抓正文工具
抓取时带上 User-Agent(部分站点对空 UA 返回 403),解析失败/超时都返回一句人话,而不是抛异常:
def fetch_web(url: str, max_len: int = 2000) -> str:
"""抓取网页纯文本正文,供模型当参考资料。"""
try:
r = requests.get(url, timeout=10,
headers={"User-Agent": "Mozilla/5.0 (kb-agent demo)"})
r.raise_for_status()
r.encoding = r.apparent_encoding or "utf-8"
return clean_html(r.text, max_len)
except requests.exceptions.RequestException as e:
return f"抓取失败:{e}"
清洗结果离线即可验证(先生成一个本地测试页):
html = """<html><body><h1>网页检索测试</h1>
<p>智能体通过 <b>工具</b> 获得外部信息。</p>
<script>var secret = 1;</script></body></html>"""
print(clean_html(html))
# 输出:网页检索测试
# 输出:智能体通过 工具 获得外部信息。
script 被整段剔除,标签与多余空白被清理,剩下的正是要喂给模型的正文。
问答流程:检索 → 组装上下文 → 带来源回答
把两个工具串成"搜索+回答"函数:搜到候选后,摘录标题/摘要与链接作为上下文,并要求模型引用来源:
# demo_web_rag.py —— 第 14 章 完整流程(需 .env,见第 2 章)
from llm_client import LLMClient # 沿用第 2 章客户端
from web_tools import search_web, fetch_web
llm = LLMClient()
def answer_with_sources(question: str) -> str:
hits = search_web(question) # 1) 搜索候选
if hits.startswith("没有搜到"):
return "没检索到相关资料,建议换个说法再问。"
first_url = next(l.strip() for l in reversed(hits.splitlines()) if l.strip())
# 上面取最后一行的链接(URL 每行只有一个,且排在条目的最后一行)
body = fetch_web(first_url) # 3) 抓正文做上下文(可选,省 token 可省)
context = (f"检索摘要:\n{hits}\n\n正文片段(来自 {first_url}):\n{body}")
system = ("你是资料检索助手。只依据上面给出的资料回答;"
"末尾用一行给出来源:来源:标题 —— URL。资料不足就明说不知道。")
return llm.chat_text([{"role": "system", "content": system},
{"role": "user",
"content": f"问题:{question}\n\n{context}"}])
输出效果(实际内容以模型与检索结果为准):
# 输出(示例):
# 用户:什么是 RAG?
# 助手:RAG(检索增强生成)是让大模型先从外部资料库检索相关内容,
# 再结合这些内容生成回答的技术,能减少幻觉、补充最新知识。
# 来源:检索增强生成 —— https://zh.wikipedia.org/wiki/检索增强生成
把 search_web、fetch_web 用第 5 章 @tool 装饰器注册后,第 8 章 run_agent 循环里模型会自己决定"什么时候搜、搜几条"——多跳问题(先搜概念再追细节)也能通过连续工具调用完成,工具内部仍然复用上面的检索与清洗函数。
没有公网时的联调办法
search 接口换成能访问的即可;若只想联调"抓取→清洗→回答"这段,起个本地静态页当抓取目标:
# 终端 1:在放有测试 HTML 的目录起静态服务
python -m http.server 8000
# 终端 2:把 fetch_web 指向本地页面验证清洗与回答链路
小结:网页检索问答 = search_web 搜候选 + clean_html/fetch_web 抓正文并清洗 + 把"摘要与正文"拼进提示词让模型基于资料作答并附来源;搜索接口可替换为你可访问的服务,断网时用本地页面联调抓取与清洗链路。