一个命令行天气助手:用自然语言询问某个城市的天气,Agent 通过 Azure OpenAI 的 function calling 调用 Open-Meteo(免密钥)查询并返回该城市的当前实时天气。
你> 北京今天天气怎么样?
助手> 北京当前晴,气温 28.3°C(体感 30.1°C),湿度 45%,风速 12.5 km/h。
- 🗣️ 自然语言交互:终端 REPL 对话,直接问"上海天气怎么样"。
- 🔧 Function Calling:天气查询作为一个工具(
get_weather)暴露给 LLM,由模型自己决定何时调用。 - 🌤️ 免密钥天气源:Open-Meteo,自带 geocoding(城市名 → 经纬度),无需注册。
- 🪝 Hook 机制:在 Agent 生命周期点(调用 LLM、函数调用、出错)触发回调,内置日志/观测 hook,可扩展监控、权限等。
- 🛡️ 健壮:城市查不到、网络/服务出错都会优雅提示,REPL 不崩。
- ✅ 全程可测:41 个单元测试,外部依赖全部 mock,离线即可运行。
手写的简短 tool-calling 循环串起若干个职责单一的模块:
| 模块 | 职责 |
|---|---|
config.py |
从环境变量(含 .env)加载并校验 Azure 配置 |
weather.py |
对接 Open-Meteo:geocode + 当前天气 + WMO 天气码映射 + 错误处理 |
tools.py |
function-calling 层:工具 JSON schema + 把 tool_call 分发到具体实现 |
agent.py |
编排器:持有 AzureOpenAI client,跑 tool-calling 循环,在生命周期点触发 hook |
hooks.py |
Hook 机制:HookManager(注册/触发)+ 内置 logging_hook |
colors.py |
终端 ANSI 上色工具(仅在真实终端上色,管道/文件保持纯文本) |
cli.py |
交互式 REPL 入口 |
用户输入 ─► agent.handle(user_msg)
├─ 追加 user message,emit before_llm_call
├─ 调用 Azure OpenAI(messages + tools schema),emit after_llm_call
├─ 模型返回 tool_calls?
│ ├─ 是 ─► emit before_tool_call → tools.dispatch → Open-Meteo
│ │ → emit after_tool_call → 工具结果回填历史 ►► 再问一轮
│ └─ 否 ─► 最终回答 ─► 返回给 CLI 打印
└─ 出错 ─► emit on_error → 友好提示,REPL 不崩
(max_tool_iterations = 5,防止工具反复触发导致死循环)
Python · openai(AzureOpenAI client)· requests(Open-Meteo)· python-dotenv · pytest
复制 .env.example 为 .env,填入你的 Azure OpenAI API key:
AZURE_OPENAI_API_KEY=<你的-key>
AZURE_OPENAI_ENDPOINT=https://hgzch-mfb0pfgz-eastus2.cognitiveservices.azure.com/
AZURE_OPENAI_API_VERSION=2024-12-01-preview
AZURE_OPENAI_DEPLOYMENT=gpt-5-mini
.env已被.gitignore排除,不会进 git。API key 只通过环境变量读取,绝不硬编码。AZURE_OPENAI_DEPLOYMENT填的是你在 Azure 上的部署名(不是模型名),且需要支持 function calling。
python -m venv .venv
.venv\Scripts\Activate.ps1
pip install -r requirements.txtpython -m weather_agent.cli本项目采用
src/布局且未做 pip 安装,运行模块需要把src加入导入路径。 若直接运行报找不到模块,设置PYTHONPATH=src即可:$env:PYTHONUTF8="1"; $env:PYTHONPATH="src" python -m weather_agent.cli
输入 exit / quit / 退出 结束对话。
Agent 在以下生命周期事件触发 hook,回调签名为 callback(event: str, data: dict):
| 事件 | 触发时机 | data |
|---|---|---|
before_llm_call |
每次调用 LLM 前 | iteration, message_count |
after_llm_call |
LLM 返回后 | tool_calls, content |
before_tool_call |
执行工具前 | name, arguments |
after_tool_call |
执行工具后 | name, arguments, result |
on_error |
调用出错时 | error |
CLI 默认开启内置的 logging_hook,把这些事件打印成日志。Hook 失败会被捕获并记录,绝不影响主流程。自定义 hook 示例:
from weather_agent.hooks import HookManager
from weather_agent.agent import WeatherAgent
from weather_agent.config import load_config
hooks = HookManager()
hooks.register("before_tool_call",
lambda event, data: print(f"即将调用 {data['name']}"))
agent = WeatherAgent.from_config(load_config(), hooks=hooks)
print(agent.handle("深圳天气"))Hook 目前为"观测型"(不改变控制流)。如需权限校验等"拦截型"能力,
before_tool_call是天然的扩展点。
日志写到 stderr、对话写到 stdout,按角色分色(仅在真实终端上色,管道/重定向时自动关闭):
| 输出 | 颜色 |
|---|---|
| 系统提示 / 助手回复 | 🟢 绿色 |
用户输入(你>) |
🟡 黄色 |
| 函数调用相关日志 | 🔵 蓝色 |
| 错误 | 🔴 红色 |
| 其他日志(调用 LLM、返回最终回答等) | 无色 |
运行时的典型日志:
→ 调用 LLM(第 1 轮,2 条消息)
← LLM 请求函数调用: get_weather (蓝色)
⚙ 执行函数 get_weather({'city': '上海'}) (蓝色)
✓ 函数 get_weather 返回 {...} (蓝色)
→ 调用 LLM(第 2 轮,4 条消息)
← LLM 返回最终回答
所有外部依赖(Azure OpenAI、Open-Meteo)均 mock,离线运行:
$env:PYTHONPATH="src"; python -m pytest -vweather/
├── .env.example # 环境变量模板(key 留空)
├── requirements.txt
├── pyproject.toml # pytest 配置(pythonpath=src)
├── README.md
├── src/weather_agent/
│ ├── config.py
│ ├── weather.py
│ ├── tools.py
│ ├── agent.py
│ ├── hooks.py
│ ├── colors.py
│ └── cli.py
└── tests/ # config / weather / tools / agent / hooks / colors / cli
| 情况 | 处理 |
|---|---|
| 城市查不到 | 工具返回 {"error": "city_not_found"},模型用自然语言告知用户 |
| Open-Meteo 网络/服务错误 | 工具返回 {"error": "weather_service_unavailable"} |
| Azure OpenAI 出错 | CLI 捕获并提示"AI 服务暂时不可用",REPL 不退出 |
| 工具参数非法 JSON | 退化为空参数,交给模型自纠(不崩整轮) |
| 工具调用次数过多 | max_tool_iterations(5)兜底 |