Skip to main content
Hello-Agent:构建一个能处理分步任务的智能旅行助手暨拓展

Hello-Agent:构建一个能处理分步任务的智能旅行助手暨拓展

Jun 6, 2026
1578words · 8minutes
AI summaryAI generated, for a quick overview

Hello

为了能从 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 模式的核心——模型看到工具返回后,可能再调下一个工具,也可能直接给最终答案。

‍


Thanks for reading!

Hello-Agent:构建一个能处理分步任务的智能旅行助手暨拓展

Jun 6, 2026
1578 words · 8 minutes

Comments