实战:搜索引擎 + 计算器 Agent

本节构建一个实用的搜索与计算 Agent,能够回答需要实时信息和数学推理的复杂问题。

搜索引擎+计算器 Agent 架构

项目目标

构建一个 Agent,能够:

  • 🔍 搜索互联网获取实时信息
  • 🧮 精确执行数学计算
  • 🔗 组合多个工具完成复杂任务
  • 📝 给出有来源依据的回答

设计思路

这个 Agent 的设计核心是工具组合:很多现实中的问题不是单一工具能解决的。比如"地球到月球多远?光飞一趟要多少秒?"——这需要先搜索距离数据,再用计算器做除法。我们的 Agent 需要具备以下能力:

  1. 理解意图:判断用户的问题需要哪些工具(或不需要工具)
  2. 多步推理:先获取信息,再基于信息做计算,最后综合回答
  3. 错误恢复:搜索失败时建议换关键词,计算出错时提示格式

我们设计三个互补的工具:search_web 获取实时信息、calculate 做精确计算、unit_converter 处理单位转换。Agent 的系统提示(System Prompt)会指导它在什么场景下使用哪个工具。

完整实现

# search_calc_agent.py
import json
import math
import os
from typing import Optional
from openai import OpenAI
from dotenv import load_dotenv
import requests
from rich.console import Console
from rich.panel import Panel
from rich.markdown import Markdown

load_dotenv()

client = OpenAI()
console = Console()

# ============================
# 工具实现
# ============================

def search_web(query: str, num_results: int = 5) -> str:
    """
    使用 DuckDuckGo 搜索引擎搜索信息(免费,无需 API Key)
    
    Returns:
        搜索结果的摘要,包含标题、链接和摘要
    """
    try:
        # 使用 DuckDuckGo Instant Answer API
        url = "https://api.duckduckgo.com/"
        params = {
            "q": query,
            "format": "json",
            "no_html": 1,
            "skip_disambig": 1,
        }
        
        response = requests.get(url, params=params, timeout=10)
        data = response.json()
        
        results = []
        
        # 直接答案
        if data.get("AbstractText"):
            results.append(f"**即时答案**:{data['AbstractText']}")
            if data.get("AbstractURL"):
                results.append(f"来源:{data['AbstractURL']}")
        
        # 相关主题
        for topic in data.get("RelatedTopics", [])[:num_results]:
            if isinstance(topic, dict) and topic.get("Text"):
                results.append(f"• {topic['Text'][:200]}")
        
        if results:
            return "\n".join(results)
        else:
            return f"搜索 '{query}' 未找到直接结果。建议换用更具体的关键词重新搜索。"
            
    except Exception as e:
        return f"搜索失败:{str(e)}。请检查网络连接。"


def calculate(expression: str) -> str:
    """
    计算数学表达式,支持复杂计算和数学函数。
    
    支持的操作:
    - 基本运算:+ - * / ** (幂运算)
    - 数学函数:sqrt, sin, cos, tan, log, log10, exp, abs, round, ceil, floor
    - 常量:pi, e
    - 括号和优先级
    
    示例:
    - calculate("(1 + 2) * 3") → 9
    - calculate("sqrt(144)") → 12.0
    - calculate("log(e)") → 1.0
    """
    try:
        # 清理输入
        expression = expression.strip()
        
        # 安全的数学环境
        safe_dict = {
            "__builtins__": {},
            "sqrt": math.sqrt,
            "pow": math.pow,
            "sin": math.sin,
            "cos": math.cos,
            "tan": math.tan,
            "asin": math.asin,
            "acos": math.acos,
            "atan": math.atan,
            "log": math.log,
            "log10": math.log10,
            "log2": math.log2,
            "exp": math.exp,
            "abs": abs,
            "round": round,
            "ceil": math.ceil,
            "floor": math.floor,
            "factorial": math.factorial,
            "pi": math.pi,
            "e": math.e,
            "inf": math.inf,
        }
        
        result = eval(expression, safe_dict)
        
        # 格式化输出
        if isinstance(result, float):
            if result == int(result):
                return f"{expression} = {int(result)}"
            else:
                return f"{expression} = {result:.6g}"
        else:
            return f"{expression} = {result}"
            
    except ZeroDivisionError:
        return "计算错误:除以零"
    except OverflowError:
        return "计算错误:结果溢出(数值太大)"
    except Exception as e:
        return f"计算错误:{str(e)}。请确认表达式格式正确。"


def unit_converter(value: float, from_unit: str, to_unit: str) -> str:
    """
    单位换算工具,支持常见的单位转换。
    
    支持的类别:
    - 长度:m, km, cm, mm, inch, foot, mile, yard
    - 重量:kg, g, mg, pound, ounce, ton
    - 温度:celsius, fahrenheit, kelvin
    - 面积:m2, km2, cm2, acre, hectare
    """
    # 转换为基础单位(SI单位)的系数
    conversions = {
        # 长度(基础单位:米)
        "m": 1.0, "km": 1000.0, "cm": 0.01, "mm": 0.001,
        "inch": 0.0254, "foot": 0.3048, "mile": 1609.344, "yard": 0.9144,
        
        # 重量(基础单位:千克)
        "kg": 1.0, "g": 0.001, "mg": 0.000001,
        "pound": 0.453592, "ounce": 0.0283495, "ton": 1000.0,
        
        # 面积(基础单位:平方米)
        "m2": 1.0, "km2": 1000000.0, "cm2": 0.0001,
        "acre": 4046.86, "hectare": 10000.0,
    }
    
    from_unit = from_unit.lower()
    to_unit = to_unit.lower()
    
    # 温度特殊处理
    if from_unit in ["celsius", "fahrenheit", "kelvin"]:
        if from_unit == "celsius" and to_unit == "fahrenheit":
            result = value * 9/5 + 32
        elif from_unit == "fahrenheit" and to_unit == "celsius":
            result = (value - 32) * 5/9
        elif from_unit == "celsius" and to_unit == "kelvin":
            result = value + 273.15
        elif from_unit == "kelvin" and to_unit == "celsius":
            result = value - 273.15
        elif from_unit == "fahrenheit" and to_unit == "kelvin":
            result = (value - 32) * 5/9 + 273.15
        elif from_unit == "kelvin" and to_unit == "fahrenheit":
            result = (value - 273.15) * 9/5 + 32
        else:
            result = value
        return f"{value} {from_unit} = {result:.4g} {to_unit}"
    
    # 其他单位
    if from_unit not in conversions or to_unit not in conversions:
        return f"不支持的单位:{from_unit} 或 {to_unit}"
    
    # 检查是否同类单位(简单判断:使用同一比较基准)
    result = value * conversions[from_unit] / conversions[to_unit]
    return f"{value} {from_unit} = {result:.6g} {to_unit}"


# ============================
# 工具配置
# ============================

TOOLS = [
    {
        "type": "function",
        "function": {
            "name": "search_web",
            "description": """在互联网上搜索实时信息。
            
适合用于:
- 查询最新新闻、事件、价格等实时数据
- 获取人物、地点、事件的背景信息
- 查找技术文档和教程
- 核实和验证信息

不适合用于:
- 数学计算(使用 calculate 工具)
- 单位换算(使用 unit_converter 工具)
- 你已经知道答案的问题""",
            "parameters": {
                "type": "object",
                "properties": {
                    "query": {
                        "type": "string",
                        "description": "搜索关键词,建议简洁精准。例如:'Python 3.12 新特性' 而非 '我想知道Python最新版本有什么新功能'"
                    },
                    "num_results": {
                        "type": "integer",
                        "description": "返回结果数量,默认5,最大10",
                        "default": 5
                    }
                },
                "required": ["query"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "calculate",
            "description": """精确计算数学表达式。
            
支持:基本运算(+,-,*,/,**)、数学函数(sqrt/sin/cos/log等)、常量(pi/e)

示例:
- "1234 * 5678" → 精确乘法
- "sqrt(2) * pi" → 数学常量计算  
- "log(100) / log(10)" → 对数计算""",
            "parameters": {
                "type": "object",
                "properties": {
                    "expression": {
                        "type": "string",
                        "description": "数学表达式,使用 Python 语法。乘方用 **,不用 ^"
                    }
                },
                "required": ["expression"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "unit_converter",
            "description": "单位换算,支持长度、重量、温度、面积等常见单位转换",
            "parameters": {
                "type": "object",
                "properties": {
                    "value": {
                        "type": "number",
                        "description": "要换算的数值"
                    },
                    "from_unit": {
                        "type": "string",
                        "description": "原始单位,如:m, km, kg, celsius, pound"
                    },
                    "to_unit": {
                        "type": "string",
                        "description": "目标单位,如:foot, mile, pound, fahrenheit"
                    }
                },
                "required": ["value", "from_unit", "to_unit"]
            }
        }
    }
]

TOOL_FUNCTIONS = {
    "search_web": search_web,
    "calculate": calculate,
    "unit_converter": unit_converter,
}


# ============================
# Agent 类
# ============================

class SearchCalcAgent:
    """搜索 + 计算 Agent"""
    
    def __init__(self):
        self.messages = [
            {
                "role": "system",
                "content": """你是一个能够搜索和计算的智能助手。

你有三个工具:
1. search_web:搜索互联网获取实时信息
2. calculate:精确计算数学表达式
3. unit_converter:单位换算

使用策略:
- 遇到需要实时数据的问题(价格、新闻、天气等)→ 先搜索
- 遇到数学计算 → 直接使用计算器,不要手动计算
- 遇到单位转换 → 使用 unit_converter
- 复杂问题可以组合使用多个工具
- 给出答案时,说明信息来源

回答要求:
- 简洁准确,重点突出
- 数字计算结果要精确
- 如果搜索结果不足,诚实说明
"""
            }
        ]
        self.step_count = 0
    
    def _log_tool_call(self, tool_name: str, args: dict, result: str):
        """记录工具调用日志"""
        console.print(
            Panel(
                f"[bold]工具:[/bold][yellow]{tool_name}[/yellow]\n"
                f"[bold]参数:[/bold]{json.dumps(args, ensure_ascii=False)}\n"
                f"[bold]结果:[/bold]{result[:300]}{'...' if len(result) > 300 else ''}",
                title=f"🔧 工具调用 #{self.step_count}",
                border_style="yellow",
                expand=False
            )
        )
    
    def chat(self, user_message: str) -> str:
        """与 Agent 对话"""
        self.step_count = 0
        self.messages.append({"role": "user", "content": user_message})
        
        console.print(f"\n[bold blue]👤 用户:[/bold blue]{user_message}\n")
        
        MAX_STEPS = 8
        
        while self.step_count < MAX_STEPS:
            # 调用 LLM
            response = client.chat.completions.create(
                model="gpt-4o",
                messages=self.messages,
                tools=TOOLS,
                tool_choice="auto",
                parallel_tool_calls=True
            )
            
            message = response.choices[0].message
            finish_reason = response.choices[0].finish_reason
            self.messages.append(message)
            
            # 直接回答
            if finish_reason == "stop":
                console.print(f"\n[bold green]🤖 Agent:[/bold green]")
                console.print(Markdown(message.content))
                return message.content
            
            # 工具调用
            if finish_reason == "tool_calls" and message.tool_calls:
                for tool_call in message.tool_calls:
                    self.step_count += 1
                    
                    func_name = tool_call.function.name
                    func_args = json.loads(tool_call.function.arguments)
                    
                    # 执行工具
                    func = TOOL_FUNCTIONS.get(func_name)
                    if func:
                        result = func(**func_args)
                    else:
                        result = f"错误:未知工具 {func_name}"
                    
                    self._log_tool_call(func_name, func_args, str(result))
                    
                    # 添加工具结果
                    self.messages.append({
                        "role": "tool",
                        "tool_call_id": tool_call.id,
                        "content": str(result)
                    })
        
        return "已达到最大步骤数限制"
    
    def reset(self):
        """重置对话"""
        self.messages = self.messages[:1]


# ============================
# 主程序
# ============================

def demo():
    """演示 Agent 能力"""
    agent = SearchCalcAgent()
    
    test_questions = [
        "地球到月球的距离是多少公里?换算成英里是多少?",
        "如果我每月存2000元,年利率3%,存5年能有多少钱?",
        "Python 3.12 版本有哪些重要的新特性?",
        "1英里等于多少公里?北京到上海的高铁距离大约是多少英里?",
    ]
    
    for q in test_questions:
        agent.chat(q)
        agent.reset()
        print("\n" + "="*60 + "\n")


def interactive():
    """交互式模式"""
    console.print(Panel(
        "[bold]🔍 搜索 + 计算 Agent[/bold]\n"
        "我可以搜索互联网 + 精确计算 + 单位换算\n"
        "输入 'quit' 退出,'reset' 重置对话",
        title="Agent 启动",
        border_style="green"
    ))
    
    agent = SearchCalcAgent()
    
    while True:
        user_input = input("\n💬 你:").strip()
        if not user_input:
            continue
        if user_input.lower() == "quit":
            break
        if user_input.lower() == "reset":
            agent.reset()
            console.print("[dim]对话已重置[/dim]")
            continue
        
        agent.chat(user_input)


if __name__ == "__main__":
    import sys
    if "--demo" in sys.argv:
        demo()
    else:
        interactive()

关键代码解读

上面的代码虽然看起来很长,但核心架构非常清晰,可以分为三层理解:

工具层(三个独立函数):每个工具都遵循"输入 → 处理 → 返回字符串"的模式。注意 calculate 函数使用了受限的 eval 环境——只暴露 math 模块中的函数,不暴露 __builtins__,这是安全性和功能性的权衡。search_web 使用 DuckDuckGo 免费 API,不需要 API Key,适合学习和原型开发。

Schema 层(TOOLS 列表):工具描述是影响 Agent 表现的关键。注意 search_web 的描述中不仅写了"适合用于"什么,还特意写了"不适合用于"什么——这种"正反两面"的描述方式能有效降低模型误用工具的概率。

Agent 层(SearchCalcAgent 类):chat 方法实现了标准的 Agent 循环,并设置了 MAX_STEPS = 8 的安全上限,防止模型陷入无限循环。_log_tool_call 方法使用 rich 库输出美观的日志,方便调试时观察 Agent 的推理过程。

运行测试

# 安装依赖
pip install openai python-dotenv requests rich

# 交互模式
python search_calc_agent.py

# 演示模式
python search_calc_agent.py --demo

示例对话

💬 你:地球和月球之间的距离是多少?如果用光速飞行需要多少秒?

🔧 工具调用 #1
工具:search_web
参数:{"query": "地球到月球距离公里"}
结果:地球到月球的平均距离约为384,400公里...

🔧 工具调用 #2  
工具:calculate
参数:{"expression": "384400 * 1000 / 299792458"}
结果:384400 * 1000 / 299792458 = 1.28222

🤖 Agent:
地球到月球的平均距离约为 **384,400 公里**。

光速为 299,792,458 米/秒(约30万公里/秒)。

计算:384,400 km × 1000 m/km ÷ 299,792,458 m/s ≈ **1.28 秒**

也就是说,光从地球到月球大约需要 **1.28 秒**。
(来源:搜索结果 + 精确数学计算)

小结

本章实战完成了一个功能完整的搜索+计算 Agent:

  • ✅ 三个独立工具:搜索、计算、单位换算
  • ✅ 工具并行调用
  • ✅ 精确的错误处理
  • ✅ 清晰的工具描述
  • ✅ 美观的终端输出

这个 Agent 可以作为你后续开发的基础框架,通过添加更多工具不断扩展能力。


下一节:4.6 论文解读:工具学习前沿进展