为了能从 Python 程序中访问网络 API,我们需要一个requests 库。avily-python是一个强大的 AI 搜索 API 客户端,用于获取实时的网络搜索结果
openai是 OpenAI 官方提供的 Python SDK,用于调用 GPT 等大语言模型服务,通过指令安装:
pip install requests tavily-python openai
驱动真实 LLM 的关键在于**提示工程(Prompt Engineering),**将作为system_prompt传递给 LLM
当前,许多 LLM 服务提供商(包括 OpenAI、Azure、以及众多开源模型服务框架如 Ollama、vLLM 等)都遵循了与 OpenAI API 相似的接口规范。这种标准化为开发者带来了极大的便利。智能体的自主决策能力来源于 LLM。例如OpenAICompatibleClient,它可以连接到任何兼容 OpenAI 接口规范的 LLM 服务。
这个简单的旅行助手案例,集中演示了基于Thought-Action-Observation范式的智能体所具备的四项基本能力:任务分解、工具调用、上下文理解和结果合成。正是通过这个循环的不断迭代,智能体才得以将一个模糊的用户意图,转化为一系列具体、可执行的步骤,并最终达成目标。
但在运行过程中发现有以下问题:
第 2-3 轮模型返回了空内容,导致解析失败浪费了 2 次循环。这是因为 我的DeepSeek 模型在收到 Observation 后偶尔会输出空行。
所以我加一个重试机制——当输出为空时自动重新调用 LLM,让整个流程更稳定
if llm_output.startswith("错误:"):
print(f"🚫 LLM 调用彻底失败: {llm_output}\n" + "="*40)
break
核心问题:LLM(包括 DeepSeek)本质上是概率采样系统——每次根据概率分布随机选择下一个 token。这意味着同样的输入可能产生不同输出。偶尔采样路径会走进"死胡同",模型输出空字符串——API 返回 200 但 content 为 None 或 ""。
正常采样路径: "Thought" → ":" → " 查询" → "天气" → ... ✅
异常采样路径: <EOS> (模型直接选择结束) ❌ → 空输出

拓展:
当前的架构:
┌──────────────┐ ┌──────────────────┐
│ prompt.py │ ──▶ │ OpenAICompat... │ ──▶ DeepSeek API
│ (主循环) │ ◀── │ (generate方法) │ ◀──
└──────┬───────┘ └──────────────────┘
│
▼
┌──────────────┐
│ 工具调度器 │ ──▶ │ get_weather()
│ (正则解析) │ │ get_attraction()
└──────────────┘
1. 字符串解析工具调用 — 脆弱且不精确
模型输出字符串
│
▼
re.search(r"Action: (.*)", llm_output) ← 模型多写一个空格
│
▼
re.search(r"(\w+)\(", action_str) ← 模型写 get_weather (city="")
│
▼
re.findall(r'(\w+)="([^"]*)"', args_str) ← 参数是整数?嵌套引号?
问题:正则匹配容易出错,更适合用于判断。模型哪怕多一个换行、少一个引号都不行
解:因为DeepSeek 支持 原生 Function Calling,模型直接返回结构化的 JSON,由 SDK 自动解析。
2. 没有区分"系统提示"和"工具定义"
当前做法是把工具描述写死在 AGENT_SYSTEM_PROMPT 字符串里:
# 硬编码在字符串里
- `get_weather(city: str)`: 查询指定城市的实时天气。
- `get_attraction(city: str, weather: str)`: 根据城市和天气搜索...
3. 主循环没有终止条件
for i in range(5): # 写死 5 轮
如果任务不需要五轮或者需要五轮以上呢?我认为没有动态判断!
改动后架构层:
┌─────────────────────────────────────────────────┐
│ 第 6 层: 交互入口 │
│ while True → input() → run_agent() │
├─────────────────────────────────────────────────┤
│ 第 5 层: Agent 主循环 │
│ while turn ≤ 10: chat() → 检查 tool_calls │
│ → 有 → 执行工具 → 追加 tool 消息 → 继续 │
│ → 无 → 输出 content → 结束 │
├─────────────────────────────────────────────────┤
│ 第 4 层: 配置 (API Key / URL / Model) │
├─────────────────────────────────────────────────┤
│ 第 3 层: DeepSeekClient.chat() │
│ 发送 messages + tools → 返回 Message 对象 │
│ 内置重试: 网络异常 / 空响应 │
├─────────────────────────────────────────────────┤
│ 第 2 层: 工具实现 (get_weather / get_attraction) │
│ 纯函数,与框架完全解耦 │
├─────────────────────────────────────────────────┤
│ 第 1 层: 工具 Schema (JSON 定义) │
│ 同时用于: API tools 参数 + 自动生成提示词 │
└─────────────────────────────────────────────────┘
数据流Before:
LLM 输出字符串
│ "Thought: xxx\nAction: get_weather(city=\"\")"
▼
re.search(r"Action: (.*)") ← 匹配失败,一轮浪费
│ "get_weather(city=\"北京\")"
▼
re.search(r"(\w+)\(") ← 工具名提取
re.findall(r'(\w+)="([^"]*)"') ← 参数提取
│
▼
available_tools["get_weather"](**kwargs)
数据流After:
LLM 返回 Message 对象
│ .tool_calls = [{
│ function: { name: "get_weather", arguments: '{"city":""}' }
│ }]
▼
json.loads(tool_call.function.arguments)
│ {"city": ""} ← 已经是 Python dict,零解析
▼
TOOL_MAP["get_weather"](**{"city": "北京"}) ← 直接执行
添加新工具
# 1. 在 TOOLS 列表中加 Schema
{
"type": "function",
"function": {
"name": "get_restaurant",
"description": "搜索指定城市的美食餐厅",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "城市名称"},
"cuisine": {"type": "string", "description": "菜系,如'川菜'、'日料'"}
},
"required": ["city"]
}
}
}
# 2. 实现函数 + 注册到 TOOL_MAP
def get_restaurant(city: str, cuisine: str | None = None) -> str:
...
TOOL_MAP["get_restaurant"] = get_restaurant
# 这样提示词自动更新,主循环自动调度。
关键点:
content: null— Function Calling 模式下,模型决定调用工具时不输出文本,content就是null。这也是为什么重试检测要区分"有 tool_calls(正常)"和"没 tool_calls 且没 content(异常)"arguments是一个 JSON 字符串,不是对象。所以代码里必须json.loads()才能拿到{"city": "城市"}id: "call_abc123"— 这个就是tool_call_id,后面回传时必须带上。
_build_tool_description() 做了什么
# 遍历同一个 TOOLS 列表,自动拼出:
# - `get_weather(city: str)`: 查询指定城市的实时天气。返回天气描述和气温。
# - `get_attraction(city: str, weather: str)`: 根据城市和天气搜索推荐的旅游景点...
为什么存在:Function Calling 的 tools 参数是发给 API 的(模型据此决策),系统提示词是给我们调试看的。而两份信息来自同一份 Schema,改了 TOOLS 就不用担心提示词忘记更新QWQ。
continue :执行完工具后重新进入循环,把带有工具结果的新 messages 再次发给 LLM。这是 ReAct 模式的核心——模型看到工具返回后,可能再调下一个工具,也可能直接给最终答案。
