Day 2:给 Agent 装上真实工具——它能真的上网搜索了
《7天从零手搓 AI Agent》第2篇 · 今日成果:Agent 能真正调用搜索 API,返回真实信息
大家好,欢迎来到小撒的私房菜,我是小撒。
昨天我们用了假天气数据。
今天换成真的。
但今天比昨天重要的,不是"换成真实 API"这件事本身,而是我们要重新想清楚"工具调用"是怎么一回事。
因为以后你的 Agent 可能有10个工具、20个工具。如果每次都手动写 if/else,会乱成一锅粥。
今天我们建一个干净的工具系统。
先想清楚一个问题
AI 是怎么知道"它有什么工具可以用"的?
答案很朴素:你用文字告诉它。
在 System Prompt 里写一段话,描述每个工具叫什么名字、用来干什么、需要什么参数。AI 读了这段话,就知道"哦,我有 web_search 工具,适合搜索互联网信息,需要一个 query 参数"。
工具调用的本质 = 靠 Prompt 里的说明书。
这说明书写得好,AI 选工具就准。写得模糊,AI 就容易选错或者用错参数。
这是今天最值得记住的一句话。
真实搜索工具:DuckDuckGo
用 DuckDuckGo 的搜索 API:完全免费,不需要注册,不需要 Key。
新建 tools/ 目录,然后创建 tools/search.py:
# tools/search.py
import requests
def web_search(query: str, max_results: int = 3) -> str:
"""用 DuckDuckGo 搜索,返回前几条结果。"""
url = "https://api.duckduckgo.com/"
params = {
"q": query,
"format": "json",
"no_html": "1",
"skip_disambig": "1",
}
try:
response = requests.get(url, params=params, timeout=10)
response.raise_for_status()
data = response.json()
results = []
# 有摘要就先加摘要
if data.get("AbstractText"):
results.append(f"摘要:{data['AbstractText']}")
# 再加相关话题
for topic in data.get("RelatedTopics", [])[:max_results]:
if isinstance(topic, dict) and topic.get("Text"):
results.append(f"- {topic['Text']}")
if results:
return "\n".join(results)
else:
return f"没有找到关于「{query}」的搜索结果"
except requests.Timeout:
return "搜索超时,请稍后重试或换个关键词"
except Exception as e:
return f"搜索出错:{e}"
几个细节值得注意:
timeout=10:网络请求必须加超时,否则网络不好时程序会一直卡在那里不动。
所有异常都被捕获并返回字符串:工具函数永远不应该让程序崩溃,出错就返回一条说明文字,让 Agent 知道工具出问题了,自己决定下一步怎么办。
返回值是字符串:工具的返回值要是 AI 能读懂的文本,不要返回复杂对象。
工具注册表:核心设计
这是今天最重要的文件,tool_registry.py:
# tool_registry.py
from tools.search import web_search
from tools.weather import get_weather # 沿用昨天的 mock
TOOLS: dict[str, dict] = {
"web_search": {
"function": web_search,
"description": "搜索互联网上的信息。适合查找新闻、事实、最新资讯。",
"parameters": {
"query": "搜索关键词,字符串类型,例如:'Python 教程'"
},
},
"get_weather": {
"function": get_weather,
"description": "查询某个城市的天气情况。",
"parameters": {
"city": "城市名称,字符串类型,例如:北京、上海"
},
},
}
def get_tools_description() -> str:
"""生成给 AI 看的工具说明文字。"""
lines = ["你有以下工具可以使用:\n"]
for name, info in TOOLS.items():
lines.append(f"工具名:{name}")
lines.append(f"用途:{info['description']}")
lines.append(f"参数:{info['parameters']}")
lines.append("")
return "\n".join(lines)
def execute_tool(tool_name: str, params: dict) -> str:
"""执行指定工具,返回结果字符串。"""
if tool_name not in TOOLS:
return f"没有这个工具:{tool_name!r},可用工具:{list(TOOLS.keys())}"
tool_func = TOOLS[tool_name]["function"]
try:
return str(tool_func(**params))
except TypeError as e:
return f"工具参数有误:{e}"
except Exception as e:
return f"工具执行出错:{e}"
这个设计的好处是:
以后想加一个新工具,只需要两步:
- 写一个 Python 函数
- 在
TOOLS字典里加一条记录
Agent 下次运行就自动学会了。不需要动任何其他文件。
更新 agent.py:把工具描述注入进去
# agent.py
import json
import re
from llm import chat
from tool_registry import get_tools_description, execute_tool
def safe_parse_json(text: str) -> dict:
try:
return json.loads(text)
except json.JSONDecodeError:
pass
match = re.search(r'\{.*\}', text, re.DOTALL)
if match:
try:
return json.loads(match.group())
except json.JSONDecodeError:
pass
return {"action": "answer", "content": text}
def build_system_prompt() -> str:
# 动态生成,包含所有工具的说明
tools_desc = get_tools_description()
return f"""你是一个智能助手,可以使用工具来帮助用户。
{tools_desc}
每次回复时,必须用 JSON 格式,选择以下两种之一:
1. 如果需要使用工具:
{{"action": "use_tool", "tool": "工具名", "params": {{"参数名": "参数值"}}}}
2. 如果可以直接回答:
{{"action": "answer", "content": "你的回答"}}
参数名必须和工具定义完全一致(英文)。
只返回 JSON,不要加任何解释。"""
def run_agent(user_input: str) -> str:
messages = [
{"role": "system", "content": build_system_prompt()},
{"role": "user", "content": user_input},
]
ai_response = chat(messages)
print(f"[AI 决策]: {ai_response}")
decision = safe_parse_json(ai_response)
if decision["action"] == "answer":
return decision.get("content", ai_response)
if decision["action"] == "use_tool":
tool_name = decision.get("tool", "")
params = decision.get("params", {})
print(f"[执行工具]: {tool_name},参数:{params}")
result = execute_tool(tool_name, params)
print(f"[工具结果]: {result}")
return result
return f"(未知 action:{decision.get('action')})"
注意 build_system_prompt() 是一个函数,不是常量。
因为工具列表可能会变,每次构建 prompt 时动态生成,保证 AI 看到的说明永远是最新的。
运行效果
你:最近 AI 领域有什么新闻?
[AI 决策]: {"action": "use_tool", "tool": "web_search", "params": {"query": "AI 人工智能最新新闻"}}
[执行工具]: web_search,参数:{'query': 'AI 人工智能最新新闻'}
[工具结果]: 摘要:...
Agent:摘要:...
你:上海天气怎么样?
[AI 决策]: {"action": "use_tool", "tool": "get_weather", "params": {"city": "上海"}}
[执行工具]: get_weather,参数:{'city': '上海'}
[工具结果]: 多云,18°C,南风2级
Agent:多云,18°C,南风2级
工具描述怎么写才好
工具描述质量直接影响 AI 选工具的准确率。
好的描述:
"description": "搜索互联网上的信息。适合查找新闻、事实、最新资讯。不适合计算或需要精确数值的问题。"
不好的描述:
"description": "搜索"
三个原则:
- 说清楚"适合什么":AI 才能选对
- 说清楚"不适合什么":防止 AI 用错
- 举个例子:参数说明里加例子,AI 生成参数时更准确
今天最常见的错误
错误:工具参数用了中文
AI 有时会用中文参数名,比如 {"城市": "北京"} 而不是 {"city": "北京"}。
在 System Prompt 里加一句:"参数名必须用英文,和工具定义完全一致",基本能解决。
如果还是出问题,在 execute_tool 里可以加一个参数名映射做兜底。
错误:搜索结果为空
DuckDuckGo 对部分中文关键词覆盖有限。试试换成英文关键词,或者更具体的词组。
今天的项目结构
my_agent/
├── .env
├── llm.py
├── agent.py # 更新:动态 prompt,通过注册表调用工具
├── tool_registry.py # 新增:工具注册中心
├── tools/
│ ├── __init__.py
│ ├── search.py # 新增:真实搜索工具
│ └── weather.py # 沿用:模拟天气
└── main.py
小结
今天做的两件事:
- 接入真实工具:Agent 能真正上网搜索了
- 建立工具注册表:以后加工具只需要加一条记录,不需要改任何其他地方
这个"注册表"模式,你在 LangChain 里会看到一模一样的思路,只不过它叫做 Tool 类。
明天,Day 3:《多工具,让 Agent 自己选——计算器和时间工具》
我们给 Agent 同时装上4个工具,看它能不能根据不同问题自动选对。
如有问题,欢迎评论区留言。
如果本教程对你有所帮助,留下一个免费的三连吧 ♥️!