这是什么
一个用不到 100 行 Python 写的 demo,演示大模型 agent 的核心机制:让模型自己决定调用哪个工具、解析它的调用请求、执行后把结果喂回去,循环直到它给出最终答案。
不涉及框架、不涉及 RAG、不涉及联网,只打通"模型 ↔ 工具"这根最基础的循环。
环境准备
先装 OpenAI 官方 SDK(DeepSeek 兼容这个 SDK,不用装额外的包):
bash
pip install openai
然后用 DeepSeek 的 API。选它的理由:
- 支持 OpenAI 兼容的 function calling 协议,代码和用 OpenAI 一模一样。
- 便宜,适合练手时随便跑。
- 国内可直连,不用折腾代理。
初始化 client:
python
client = OpenAI(
api_key="你的 key", # DeepSeek 后台申请
base_url="https://api.deepseek.com", # 换成 DeepSeek 端点
)
# model 用 "deepseek-v4-flash"
场景
"北京和上海、天津今天多少度,两地温差多少?顺便考虑下北京要不要带伞?"
一句话里藏了几个子任务:查北京天气、查上海天气、查天津天气(假数据里没有,会触发错误回传)、算温差。模型需要自己拆解,分多次调用工具,再综合回答------刚好够把 agent loop 的关键点全踩一遍。
前置认知:Agent 到底在循环什么
普通对话是单轮:用户问 → 模型答。
带工具的 agent 是多轮,但循环的不是用户,而是模型和工具之间的来回:
scss
用户提问
↓
模型思考 → "我需要调 get_weather('北京')"
↓
本地执行 get_weather('北京') → 返回数据
↓
数据喂回模型 → "还要查上海"
↓
本地执行 get_weather('上海') → 返回数据
↓
数据喂回模型 → "还要算温差,调 calculator('25-20')"
↓
本地执行 calculator → 返回 5
↓
模型综合所有结果 → 给出最终文本回复
关键点:模型不能直接执行代码,它只能输出"我想调哪个工具、传什么参数"的结构化请求。真正执行的是你的本地代码。你的职责是解析这个请求、执行、把结果塞回上下文。
工具定义:模型怎么知道有哪些工具
用 OpenAI 的 function calling 格式,每个工具是一个 JSON Schema:
python
tools = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "查询指定城市的天气,返回温度和是否下雨。",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "城市名称,如北京、上海",
},
},
"required": ["city"],
},
},
},
# calculator 同理
]
核心循环
python
MAX_STEPS = 10
def run_agent(user_input):
messages = [{"role": "user", "content": user_input}]
for step in range(1, MAX_STEPS + 1):
response = client.chat.completions.create(
model="deepseek-v4-flash",
messages=messages,
tools=tools,
timeout=30,
)
message = response.choices[0].message
messages.append(message)
# 没有工具调用 → 模型给出最终回复,退出循环
if not message.tool_calls:
print("最终回复:", message.content)
return
# 有工具调用 → 逐个执行,把结果回传
for tool_call in message.tool_calls:
func_name = tool_call.function.name
func_args = json.loads(tool_call.function.arguments)
result = available_functions[func_name](**func_args)
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": result,
})
上下文是怎么累积的
每轮 messages 列表按顺序追加,长这样:
user--- 用户提问assistant--- 模型决定调工具(这条消息带tool_calls)tool--- 你执行函数后的结果assistant--- 模型继续调工具或给出答案- ... 直到某条
assistant没有tool_calls
每次请求都把完整历史发回去,模型靠这个知道"我之前查过什么、还要查什么"。
回传工具结果的三要素
python
{
"role": "tool",
"tool_call_id": tool_call.id, # 必须配对
"content": result, # 字符串
}
role固定是"tool"。tool_call_id必须等于模型那条tool_call.id------模型靠它把"我刚才要的结果"和"你给的结果"对应起来。一轮里模型可能并发调多个工具,每个都要单独回传一条tool消息。content是字符串。如果函数返回 dict,要json.dumps一下。
三道护栏
裸循环容易跑飞,加三层保护:
python
MAX_STEPS = 10 # 1. 最大步数:模型陷入反复调工具时强制停
timeout=30, # 2. 单次请求超时:API 卡住时别死等
try/except # 3. 错误处理:处理异常和工具调用失败
错误处理要分两种情况:
- 模型请求失败(网络、限流)→ 直接 return,没什么可补救的。
- 工具执行失败 (比如查不到天津)→ 别中断,把错误也回传给模型:
python
except Exception as e:
result = f"工具执行失败:{e}"
# 照常 append 成 role=tool 消息
这样模型会知道"天津查不到",自己决定是换个问法还是直接告知用户。这比直接崩掉友好得多。
实际跑起来什么样
输入:"北京和上海、天津今天多少度,两地温差多少?顺便北京要不要带伞?"
lua
--- 第 1 步 ---
调用工具:get_weather({'city': '北京'})
调用工具:get_weather({'city': '上海'})
调用工具:get_weather({'city': '天津'}) ← 假数据里没有,触发异常
--- 第 2 步 ---
调用工具:calculator({'expression': '25 - 20'})
--- 第 3 步 ---
最终回复:北京 20 度不用带伞,上海 25 度,温差 5 度;天津暂无数据。
一次对话里串起了并发工具调用、结构化参数、错误回传、最终综合------agent loop 的几个关键环节全过一遍。
完整代码
python
from openai import OpenAI
import json
client = OpenAI(
api_key="你的 key",
base_url="https://api.deepseek.com",
)
WEATHER_DATA = {
"北京": {"temp": 20, "rain": False},
"上海": {"temp": 25, "rain": True},
}
tools = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "查询指定城市的天气,返回温度和是否下雨。",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "城市名称,如北京、上海",
},
},
"required": ["city"],
},
},
},
{
"type": "function",
"function": {
"name": "calculator",
"description": "计算数学表达式,例如 '25 - 20'。",
"parameters": {
"type": "object",
"properties": {
"expression": {
"type": "string",
"description": "要计算的数学表达式",
},
},
"required": ["expression"],
},
},
},
]
def get_weather(city):
if city not in WEATHER_DATA:
raise ValueError(f"暂不支持城市:{city}")
return json.dumps(WEATHER_DATA[city], ensure_ascii=False)
def calculator(expression):
return str(eval(expression))
available_functions = {
"get_weather": get_weather,
"calculator": calculator,
}
MAX_STEPS = 10
def run_agent(user_input):
messages = [{"role": "user", "content": user_input}]
for step in range(1, MAX_STEPS + 1):
print(f"\n--- 第 {step} 步 ---")
try:
response = client.chat.completions.create(
model="deepseek-v4-flash",
messages=messages,
tools=tools,
timeout=30,
)
except Exception as e:
print(f"模型请求失败:{e}")
return
message = response.choices[0].message
messages.append(message)
if not message.tool_calls:
print("最终回复:", message.content)
return
for tool_call in message.tool_calls:
func_name = tool_call.function.name
try:
func_args = json.loads(tool_call.function.arguments)
print(f"调用工具:{func_name}({func_args})")
result = available_functions[func_name](**func_args)
except Exception as e:
result = f"工具执行失败:{e}"
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": result,
})
print(f"已达到最大步数 {MAX_STEPS},强制停止。")
run_agent("北京和上海、天津今天多少度,两地温差多少?顺便北京要不要带伞?")