实战指南:用 Python 从零构建一个具有全网搜索能力的 Agent
实战指南:用 Python 从零构建一个具有全网搜索能力的 Agent
一、 引言 (Introduction)
钩子 (The Hook)
想象一下:你有一个不知疲倦的数字助手,当你问它“今天英伟达的股价是多少?”或者“最新的 AI 研究论文有什么突破性进展?”时,它不会仅仅依赖于它那过时的知识库,而是能够像你我一样,打开浏览器,在茫茫互联网中寻找最新的信息,经过筛选、整合,最后给你一个条理清晰、有时效性的答案。这听起来像是科幻电影,但今天,我们就要用 Python 把它变成现实。
你是否已经厌倦了 LLM(大语言模型)一本正经地胡说八道?是否因为它们的知识截止日期而无法获取实时资讯?如果是,那么这篇文章就是为你准备的。
定义问题/阐述背景 (The “Why”)
在当今这个信息爆炸的时代,LLMs(如 GPT-4、Claude)展现出了惊人的理解和生成能力。然而,它们有一个致命的缺陷:知识 cutoff(知识截止期)。它们就像是被时间胶囊封印住了,对截止日期之后发生的事情一无所知。此外,它们也无法访问你个人的私有数据。
这就是 Agent(智能体) 概念火爆的原因。我们不再满足于让 LLM 仅仅作为一个“内容生成器”,我们要让它成为一个能够使用工具、与外界交互、具备推理能力的“行动者”。而赋予 Agent“全网搜索”的能力,就像是给它装上了一双洞察世界的眼睛。
亮明观点/文章目标 (The “What” & “How”)
在这篇万字长文中,我将带你摒弃那些复杂的框架(暂时忘记 LangChain、AutoGPT),用最原生的 Python 代码,从零开始解构并构建一个具有以下能力的 Search Agent:
- 思考与规划 (Reasoning): 分析用户的问题,判断是否需要搜索。
- 工具调用 (Tool Calling): 学习如何让 LLM 生成函数调用指令来使用搜索引擎。
- 全网搜索 (Web Search): 集成 Search API 获取实时信息。
- 结果整合 (Synthesis): 将搜索到的零散信息总结成最终答案。
我们将使用 OpenAI 的 GPT 模型作为核心推理引擎,并通过一步步的代码迭代,最终打造出一个属于你自己的智能搜索助手。坐稳了,我们要开始发车了!
二、 基础知识/背景铺垫 (Foundational Concepts)
在我们开始撸起袖子写代码之前,有几个核心概念必须搞清楚。这将帮助你理解后面每一行代码背后的“为什么”。
2.1 什么是 Agent (智能体)?
核心概念:
在 AI 领域,Agent 是一个能够感知环境、做出决策并执行行动的自主实体。放在 LLM 的语境下,一个典型的 Agent 循环通常包含以下几个部分:
- Perception (感知): 接收用户的输入或环境反馈。
- Thought (思考): LLM 分析当前状态,决定下一步做什么。
- Action (行动): 执行决策(例如调用搜索工具、查询数据库)。
- Observation (观察): 获取行动的结果。
- Loop (循环): 将观察结果送回 LLM,重复上述过程,直到任务完成。
我们可以用一个经典的公式来描述 Agent 的行为:
Agent:Percepts→ActionsAgent: Percepts \rightarrow ActionsAgent:Percepts→Actions
2.2 Function Calling (函数调用) 的魔力
要让 Agent 具备“搜索”能力,最关键的技术就是 Function Calling(有时也叫 Tool Use)。
问题背景:
LLM 本质上是一个文本生成模型。在过去,如果你想让它帮你查天气,你只能在 Prompt 里苦苦哀求:“如果用户问天气,请以 JSON 格式返回地理位置,谢谢。” 然后你还要写一堆正则表达式去解析那个不靠谱的 JSON。
核心概念:
现代 LLM(如 GPT-3.5/4-turbo)通过 Fine-tune 具备了一项新能力:当你在 API 中传入一组工具(Functions)的定义时,LLM 会智能地判断什么时候该调用什么工具,并返回给你标准的、可以直接执行的参数,而不是瞎编一个答案。
这就好比你给了实习生一张《工具使用说明书》,他看了一眼任务,说:“老板,这个问题我需要先用‘搜索引擎’查一下资料,这是我打算输入的关键词,您批准不?”
2.3 技术栈选型
为了保持“从零开始”的纯粹性,同时又能快速见效,我们选择以下极简技术栈:
| 组件 | 选型 | 理由 |
|---|---|---|
| 编程语言 | Python 3.10+ | 生态最好,异步支持完善。 |
| 推理核心 | OpenAI GPT-4o / GPT-3.5-turbo | 目前 Function Calling 能力最稳定的模型。 |
| 搜索能力 | SerpAPI / DuckDuckGo | 可以轻松获取 Google/Bing 搜索结果,无需复杂配置。 |
| HTTP 库 | httpx |
支持异步,比 requests 更现代化。 |
| 环境管理 | python-dotenv |
管理 API Key 等敏感信息。 |
注:你不需要 LangChain 也能构建强大的 Agent。理解底层逻辑后,再去用框架会事半功倍。
三、 核心内容/实战演练 (The Core - “How-To”)
好了,概念讲得差不多了。现在让我们进入最激动人心的部分:Coding! 我们将分为三个阶段来迭代我们的 Agent。
3.1 环境准备与热身:徒手调用 LLM
首先,我们要搭建一个干净的开发环境。
3.1.1 项目初始化
打开你的终端,执行以下命令:
mkdir search-agent && cd search-agent
python -m venv venv
# Windows 激活: .\venv\Scripts\activate
# Mac/Linux 激活: source venv/bin/activate
pip install openai httpx python-dotenv
touch .env main.py
在 .env 文件中填入你的密钥:
# .env
OPENAI_API_KEY=sk-xxxxxxxxxxxxx
SERPAPI_API_KEY=xxxxxxxxxxxxxxx # 稍后去 serpapi.com 申请一个免费的
3.1.2 与 LLM 对话的最小原型
在写 Agent 之前,我们先写一段最简单的代码来熟悉 OpenAI SDK 的运作方式。这是我们后续所有逻辑的基础。
# main.py
import os
from dotenv import load_dotenv
from openai import OpenAI
# 加载环境变量
load_dotenv()
# 初始化客户端
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
def basic_chat(query: str):
messages = [
{"role": "system", "content": "你是一个乐于助人的助手。"},
{"role": "user", "content": query}
]
response = client.chat.completions.create(
model="gpt-3.5-turbo",
messages=messages
)
return response.choices[0].message.content
if __name__ == "__main__":
print(basic_chat("你好,请介绍一下你自己。"))
这很简单,对吧?但这只是一个“复读机”模式。如果我们问它:“今天北京天气怎么样?” 它会说:“我无法提供实时天气数据……”
是时候给它植入“搜索”芯片了。
3.2 关键一步:集成 Function Calling
现在,我们要修改上面的代码,教 LLM 学会“使用工具”。
3.2.1 定义搜索工具 (Tools Definition)
我们需要把“搜索”这个能力用 JSON Schema 的方式描述给 LLM 听。
# 在 main.py 中添加
import json
import httpx
def search_web(query: str) -> str:
"""
利用 SerpAPI 搜索网络
"""
print(f"[Agent Action] 正在搜索: {query}")
api_key = os.getenv("SERPAPI_API_KEY")
params = {
"engine": "google",
"q": query,
"api_key": api_key,
"num": 5 # 只取前5条结果,节省 Token
}
try:
response = httpx.get("https://serpapi.com/search", params=params, timeout=10)
data = response.json()
# 简单的结果解析,提取标题和链接
results = []
if "organic_results" in data:
for item in data["organic_results"][:3]: # 只取前3条最相关的
results.append({
"title": item.get("title"),
"link": item.get("link"),
"snippet": item.get("snippet")
})
return json.dumps(results, ensure_ascii=False)
except Exception as e:
return f"搜索出错: {str(e)}"
# 这是给 LLM 看的“工具说明书”
tools = [
{
"type": "function",
"function": {
"name": "search_web",
"description": "当你需要回答实时信息、最新新闻、或者不确定的知识时,使用这个工具搜索互联网。",
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "搜索关键词或短语",
},
},
"required": ["query"],
},
},
}
]
这里的关键点在于 tools 这个列表。我们清晰地定义了函数名、描述(告诉 LLM 什么时候用)以及参数(告诉 LLM 传什么)。
3.2.2 ReAct 推理循环的实现
现在到了核心逻辑。我们需要编写一个循环,让 LLM 能够像人一样“思考 -> 行动 -> 观察 -> 思考…”。这就是著名的 ReAct (Reasoning + Acting) 模式。
让我们画一张流程图来理清思路:
现在让我们用 Python 实现这个逻辑:
# main.py
def run_simple_agent(user_query: str):
# 1. 初始化对话历史
messages = [
{"role": "system", "content": "你是一个聪明的 AI 搜索助手。你可以使用工具来获取信息。如果有搜索结果,请基于搜索结果回答,并在最后附上参考链接。如果无法通过搜索得到答案,请诚实地说明。"}
]
messages.append({"role": "user", "content": user_query})
# 为了防止死循环,我们限制最大迭代次数
max_iterations = 5
for i in range(max_iterations):
print(f"[Agent Loop] 第 {i+1} 次思考...")
# 2. 调用 LLM,带上 tools 定义
response = client.chat.completions.create(
model="gpt-4o", # 建议使用 GPT-4o 或 3.5-turbo (1106)
messages=messages,
tools=tools,
tool_choice="auto", # 让 LLM 自己决定要不要用工具
)
response_message = response.choices[0].message
tool_calls = response_message.tool_calls
# 3. 检查 LLM 是否想要调用工具
if tool_calls:
# 将 LLM 的回复(包含 tool_calls)加入上下文
messages.append(response_message)
# 遍历执行每一个工具调用
for tool_call in tool_calls:
function_name = tool_call.function.name
function_args = json.loads(tool_call.function.arguments)
# 简单的路由逻辑
if function_name == "search_web":
function_response = search_web(
query=function_args.get("query")
)
# 将工具执行结果返回给 LLM
messages.append(
{
"tool_call_id": tool_call.id,
"role": "tool",
"name": function_name,
"content": function_response,
}
)
# 继续下一轮循环,让 LLM 看结果
else:
# LLM 没有调用工具,说明它觉得可以直接回答了
print("[Agent Finish] 生成最终答案...")
return response_message.content
return "抱歉,思考步骤过多,未能得出结论。"
# 测试一下
if __name__ == "__main__":
query = "2024年奥运会在哪里举办?金牌榜第一是哪个国家?"
print(f"用户提问: {query}")
result = run_simple_agent(query)
print("-"*20)
print(result)
让我们运行一下这段代码!你会看到控制台输出类似这样的内容:
- Agent 开始第一次思考。
- 它决定调用
search_web,参数是 “2024 Olympics host country medal table”。 - 我们的 Python 代码执行搜索,拿到一堆 JSON 数据。
- 数据塞回给 LLM。
- LLM 看了数据,整理成自然语言回答我们。
恭喜你!你已经写出了你的第一个具备全网搜索能力的 Agent!
3.3 升级:让 Agent 更像 Agent (记忆与 Prompt 优化)
上面的代码虽然能用,但还有点简陋。真正的 Agent 需要更好的 System Prompt(系统提示词) 和更清晰的内部独白。
3.3.1 高级 System Prompt 设计
让我们替换掉那个简单的 System Prompt。我们要让 LLM 用 XML 标签来规范它的思考过程(这是一种很有效的提示工程技巧)。
SYSTEM_PROMPT = """
你是一个专业的研究助手。你的目标是尽可能全面且准确地回答用户的问题。
# 工作流程
1. 你应该首先思考:我是否需要额外的信息来回答这个问题?
2. 如果问题涉及实时信息(如天气、新闻、股价)或你不确定的知识,请使用 search_web 工具。
3. 获取搜索结果后,你需要仔细阅读,并基于结果撰写最终答案。
# 回复格式
当你思考时,请使用 <thinking> 标签包裹你的内心独白。
当你给出最终答案时,请使用 <final_answer> 标签包裹,并确保引用信息来源。
"""
然后我们修改 messages 的初始化部分即可。虽然这只是一个简单的改动,但它能极大地提高 Agent 输出的可解释性。
3.3.2 上下文管理 (Context Window Stuffing)
随着搜索次数变多,messages 列表会变得很长,可能会超出 Token 限制。一个简单的优化策略是滑动窗口或者总结记忆。
这里提供一个简单的 Truncation(截断)策略:
def manage_context(messages, max_tokens=8000):
# 这是一个简化的策略,实际中可以使用 tiktoken 精确计算
# 我们永远保留第一条 (system) 和最后一条 (最新的用户输入/工具返回)
# 如果太长,我们就删除中间的一些旧消息
while len(messages) > 6: # 简单的长度限制
if messages[1]["role"] != "system": # 不要删除 system prompt
messages.pop(1)
return messages
在每次循环调用 LLM 之前,调用一下这个函数,可以有效防止 Context 爆炸。
四、 进阶探讨/最佳实践 (Advanced Topics / Best Practices)
现在你的 Agent 已经能跑了,但从“能跑”到“好用”还有很长的路要走。让我们来探讨一些进阶话题。
4.1 常见陷阱与避坑指南
4.1.1 幻觉 (Hallucination) 依然存在
问题: 即使给了搜索结果,LLM 有时候还是会瞎编,或者过度解读搜索结果中的只言片语。
解决:
- Prompt 约束: 在 System Prompt 中反复强调:“如果搜索结果中没有相关信息,请直接说不知道,不要编造。”
- 引用溯源: 强制要求 LLM 在给出的每个事实后面加上
[1]、[2]这样的标注,并对应到搜索结果的链接。
4.1.2 无限循环 (Infinite Loop)
问题: LLM 可能会陷入“搜索 -> 不满意结果 -> 换个词再搜索 -> 还不满意…”的死循环。
解决:
- 硬限制: 像我们代码里那样,设置
max_iterations。 - 反思机制 (Reflection): 在 Prompt 中加入:“如果你发现已经搜索了两次还没找到答案,请停止并告知用户目前找到的有限信息。”
4.1.3 搜索词生成得很烂
问题: LLM 有时候不会提炼关键词。比如用户问:“我想知道昨天 Apple 发布的那个Vision Pro的销量咋样了?” LLM 可能直接把这句话全塞进去搜索了。
解决: 为了获得更好的搜索结果,我们甚至可以把“生成搜索关键词”本身变成一个 LLM 调用步骤,或者优化 Function Description:
"description": "生成最优的 Google 搜索关键词。不要包含废话,只保留核心实体。例如:2024 Tesla stock price。",
4.2 架构升级:多 Agent 协作 (Multi-Agent)
如果你觉得单个 Agent 不够聪明,我们可以把工作拆解。这也是目前 Agent 领域最火的方向:Divide and Conquer (分而治之)。
我们可以引入 Mermaid ER 图 来描述一个简单的 Multi-Agent 架构:
在这个架构中:
- Boss Agent 不干活,只负责拆解任务(Task Decomposition)和分配工作。
- Searcher Agent 只负责搜索,生成最优关键词。
- Scraper Agent 负责点开搜索结果里的链接,把全文爬下来(如果有必要)。
- Analyst Agent 负责阅读一堆资料并总结。
这部分的代码实现比较复杂,但核心思想依然是我们上面讲的“Function Calling”和“Message Passing”,只不过每个 Agent 有自己专属的 System Prompt。
4.3 成本与性能优化
跑 Agent 是要花钱的(如果你用付费 API 的话)。这里有几个省钱小技巧:
- 模型分级 (Model Router): 简单的搜索词生成用 GPT-3.5-turbo,最终的答案整合用 GPT-4o。
- 结果缓存 (Caching): 如果用户问了一个类似的问题,或者搜索了同一个关键词,直接把上次存下来的结果取出来,不要重新搜也不要重新跑 LLM。
- 异步化 (Async): 虽然我们上面是顺序执行的,但实际上你可以让 Agent 同时搜索好几个不同的关键词,然后再一起处理。使用
asyncio和async_openai可以极大提升速度。
五、 结论 (Conclusion)
核心要点回顾 (The Summary)
在这篇漫长的文章中,我们从 0 到 1 构建了一个 Search Agent。让我们再次复盘一下核心心法:
- Agent 的本质:不是什么神秘的黑科技,而是一个 LLM + 工具 + 循环逻辑 的组合体。
- Function Calling:这是连接 LLM(大脑)和现实世界(手脚)的关键接口。
- ReAct 模式:Thinking -> Acting -> Observing -> Thinking… 这个循环是 Agent 智能的体现。
我们没有依赖 LangChain 这种重型框架,而是直接使用了 OpenAI SDK。这不仅让你理解了底层逻辑,而且实际上,在生产环境中,很多团队最终都会选择“自造轮子”,因为它更可控、更易调试。
展望未来/延伸思考 (The Outlook)
Agent 技术的发展可谓一日千里。我们可以预见几个趋势:
- 更长的 Context Window: 现在的 Agent 受限于 Token,只能看一点点搜索摘要。未来,直接把整个网页塞进去将成为常态。
- 更完善的工具生态: 除了搜索,Agent 还会写代码、发邮件、控制智能家居。
- 从“单 Agent”到“多 Agent 社会”: 就像我们在 4.2 节提到的,未来可能是一群专家 Agent 互相协作。
行动号召 (Call to Action)
好了,光说不练假把式。现在轮到你了:
- 立刻动手: 把文中的代码复制到你的 IDE 里跑通。
- 修改 Prompt: 尝试优化 System Prompt,让你的 Agent 回答更有“个性”(比如让它像一个脱口秀演员一样播报新闻)。
- 扩展工具: 除了
search_web,试着给它加一个calculator工具或者get_current_time工具。
如果你在构建过程中有任何有趣的发现,或者遇到了什么坑,欢迎在评论区留言交流!
推荐资源:
- OpenAI Function Calling Docs: 官方文档永远是最好的教材。
- ReAct Paper: “ReAct: Synergizing Reasoning and Acting in Language Models” (Yao et al., 2022)。
- AutoGPT (Inspiration): 虽然我们不用它,但看看它的源码对你理解 Agent 很有帮助。
感谢你阅读这篇万字长文!我们下次再见。
更多推荐


所有评论(0)