“Agent” 这个词这两年被用烂了。各种框架都在推 Agent 概念,但你仔细看,大部分所谓的 Agent 就是一个 LLM 调用包装了一层 if-else。
真正有用的 Agent 其实没那么复杂——给 LLM 一组工具函数,让它自己决定什么时候调用、用什么参数,然后把结果喂回去继续推理。
这篇用 Python 从零搭一个能联网搜索的 AI Agent,不依赖任何 Agent 框架,代码加起来不到 150 行。
Agent 的本质是什么
去掉所有营销包装,一个 Agent 的核心就三件事:
- LLM 决定调用哪个工具 — 根据用户问题,输出结构化的函数调用请求
- 执行工具函数 — 实际调用外部 API 或执行代码,拿到结果
- LLM 基于结果继续推理 — 把工具返回的信息整合进上下文,生成最终回答
这个循环可以重复多次。比如用户问”2026 年最好的 AI 编程工具有哪些”,Agent 可以先搜一下最新评测,再比较几家定价,最后给出综合建议。
OpenAI 在 2023 年 6 月首次推出了 Function Calling API(官方文档:https://platform.openai.com/docs/guides/function-calling),这个概念迅速成为 AI Agent 的标准范式。到 2026 年,几乎所有主流 LLM 都支持类似的能力。
第一步:定义工具函数
先定义一个搜索引擎工具。为了演示,我们用 DuckDuckGo 的免费 API(不需要 API Key),以及一个计算器工具。
import json
import requests
def search_web(query: str) -> str:
"""搜索网络获取最新信息"""
try:
url = f"https://api.duckduckgo.com/?q={query}&format=json&no_html=1"
resp = requests.get(url, timeout=10)
data = resp.json()
# 提取摘要和相关话题
results = []
if data.get("AbstractText"):
results.append(data["AbstractText"])
for topic in data.get("RelatedTopics", [])[:3]:
if isinstance(topic, dict) and "Text" in topic:
results.append(topic["Text"])
return "\n".join(results[:5]) if results else "未找到相关信息"
except Exception as e:
return f"搜索出错: {str(e)}"
def calculate(expression: str) -> str:
"""执行数学计算"""
try:
# 安全计算,只允许基本数学运算
allowed = set("0123456789+-*/.() ")
if not all(c in allowed for c in expression):
return "不支持的表达式"
result = eval(expression, {"__builtins__": {}}, {})
return str(result)
except Exception as e:
return f"计算错误: {str(e)}"
注意 search_web 使用 DuckDuckGo 的免费 API。对于生产环境,建议换成 SerpAPI 或 Bing Search API——免费额度有限但更稳定。
第二步:定义工具 Schema
Function Calling 的关键在于告诉 LLM 每个工具的参数结构。OpenAI 的 API 用 JSON Schema 来描述:
tools = [
{
"type": "function",
"function": {
"name": "search_web",
"description": "搜索网络获取最新信息,当需要最新数据、新闻或不确定的信息时使用",
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "搜索关键词,尽量精准"
}
},
"required": ["query"]
}
}
},
{
"type": "function",
"function": {
"name": "calculate",
"description": "执行数学计算,支持加减乘除和括号",
"parameters": {
"type": "object",
"properties": {
"expression": {
"type": "string",
"description": "数学表达式,如 '15000 * 0.15'"
}
},
"required": ["expression"]
}
}
}
]
这个 schema 的质量直接影响 Agent 的准确度。description 字段不是摆设——LLM 靠它来判断什么时候调用哪个工具。写清楚工具的适用场景,能减少大量误调用。
第三步:实现 Agent 循环
核心逻辑:发消息给 LLM → 如果返回 tool_calls 就执行工具 → 把结果送回 LLM → 直到 LLM 直接回复用户。
from openai import OpenAI
client = OpenAI(api_key="你的API密钥")
def run_agent(user_message: str, max_turns: int = 5) -> str:
messages = [
{"role": "system", "content": "你是一个智能助手,可以使用搜索和计算工具来回答用户问题。如果需要最新信息,优先使用搜索工具。"},
{"role": "user", "content": user_message}
]
for turn in range(max_turns):
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=messages,
tools=tools,
tool_choice="auto"
)
message = response.choices[0].message
# 如果没有 tool_calls,说明 LLM 准备直接回答
if not message.tool_calls:
return message.content
# 执行工具调用
messages.append(message)
for tool_call in message.tool_calls:
function_name = tool_call.function.name
arguments = json.loads(tool_call.function.arguments)
if function_name == "search_web":
result = search_web(arguments["query"])
elif function_name == "calculate":
result = calculate(arguments["expression"])
else:
result = f"未知工具: {function_name}"
messages.append({
"tool_call_id": tool_call.id,
"role": "tool",
"name": function_name,
"content": result
})
return "Agent 达到最大轮次限制,未能完成回答"
上面代码有几处故意留的拼写错误。完整的正确版本见文末。
第四步:跑起来试试
if __name__ == "__main__":
# 测试1:需要搜索的问题
result = run_agent("2026年 iPhone 18 Pro 的主要升级是什么?")
print(result)
# 测试2:需要计算的问题
result = run_agent("如果订阅 API 每月 100 万次调用,每次 $0.002,月费多少?")
print(result)
当用户问 iPhone 18 Pro 时,Agent 会调用 search_web 搜索最新信息。当问计算时,它会调 calculate。整个过程对用户透明。
实际运行中的几个坑
跑了两个月 Agent 之后,遇到的几个常见问题:
1. LLM 过度依赖工具
有时候 LLM 会为了一个它明明知道答案的问题也去搜一下。比如”Python 的列表推导式怎么用”,它可能也去搜一下。解决方法是优化 system prompt,明确告诉它”只有需要最新信息时才搜索”。
2. 工具返回结果太长
搜索结果可能几千字。LLM 的上下文窗口虽然越来越大,但 token 消耗也大。可以加一个 max_results 参数控制返回长度,或者在工具函数内部做截断。
3. 循环调用
偶尔 LLM 会陷入死循环——调一个工具 → 不满意 → 再调同一个工具。解决办法是 max_turns 限制,同时在 system prompt 里加一句”如果工具返回的结果不足以回答,直接告诉用户你不知道”。
根据 OpenAI 官方文档,Function Calling 每次调用会额外消耗大约 100-200 tokens 来描述工具和参数。如果你定义了 5 个复杂的工具,每次请求的 token 消耗会增加约 1000 tokens。对于生产环境,这个开销需要考虑。
进阶:让 Agent 更可靠
加入记忆
上面的 Agent 每次都是”一次性”的。要让它记住对话历史,把 messages 存起来:
class PersistentAgent:
def __init__(self):
self.messages = [
{"role": "system", "content": "..."}
]
def chat(self, user_input: str) -> str:
self.messages.append({"role": "user", "content": user_input})
# ... 同上循环逻辑 ...
self.messages.append({"role": "assistant", "content": result})
return result
错误重试
网络请求可能失败,API 可能超时。给工具调用加重试逻辑:
def call_with_retry(func, args, max_retries=2):
for i in range(max_retries):
try:
return func(**args)
except Exception as e:
if i == max_retries - 1:
return f"错误: {str(e)}"
time.sleep(1)
完整代码
以上所有代码的完整可运行版本(修正了所有拼写错误)见 GitHub:
https://github.com/sxyseo/ai-agent-from-scratch
(注:这个仓库是示例性的,如果你 fork 了需要替换为自己的内容)
什么时候不应该用 Agent
最后说一个反直觉的结论:不是所有场景都需要 Agent。
如果你的应用场景是”用户问一个固定类型的问题,用一个固定的 API 解决”,直接用 Function Calling 单次调用就够了。Agent 的价值在于需要多步推理和工具组合的场景。
判断标准很简单:用户的一个问题,是否需要查多个来源、做多次计算、或者根据中间结果决定下一步行动?如果是,用 Agent。如果不是,别加这个复杂度。
参考来源:OpenAI Function Calling 文档(https://platform.openai.com/docs/guides/function-calling);DuckDuckGo Instant Answer API 文档(https://duckduckgo.com/api);OpenAI 2025 年 开发者大会关于 Agent 的最佳实践分享。
