Day 2:给 Agent 装上真实工具——它能真的上网搜索了

88 阅读6分钟

Day 2:给 Agent 装上真实工具——它能真的上网搜索了

《7天从零手搓 AI Agent》第2篇 · 今日成果:Agent 能真正调用搜索 API,返回真实信息

day2.png

大家好,欢迎来到小撒的私房菜,我是小撒。

昨天我们用了假天气数据。

今天换成真的。

但今天比昨天重要的,不是"换成真实 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}"

这个设计的好处是:

以后想加一个新工具,只需要两步:

  1. 写一个 Python 函数
  2. 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": "搜索"

三个原则:

  1. 说清楚"适合什么":AI 才能选对
  2. 说清楚"不适合什么":防止 AI 用错
  3. 举个例子:参数说明里加例子,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

小结

今天做的两件事:

  1. 接入真实工具:Agent 能真正上网搜索了
  2. 建立工具注册表:以后加工具只需要加一条记录,不需要改任何其他地方

这个"注册表"模式,你在 LangChain 里会看到一模一样的思路,只不过它叫做 Tool 类。


明天,Day 3:《多工具,让 Agent 自己选——计算器和时间工具》

我们给 Agent 同时装上4个工具,看它能不能根据不同问题自动选对。


如有问题,欢迎评论区留言。

如果本教程对你有所帮助,留下一个免费的三连吧 ♥️!