0-pydanticai-openai-comm-agent.py 代码说明这份脚本和 6/2-prog/0-pydanticai-comm-agent.py 做的是同一件事:
OllamaPydanticAI但这一版更重要的教学意义在于:
因为这一次,我们不再自己写 Ollama /api/chat 适配层,而是直接使用:
OpenAIChatModelOpenAIProvider也就是说,只要新版 Ollama 的 /v1 OpenAI 兼容接口可用,很多中间翻译代码都可以省掉。
它的完整流程是:
PydanticAI AgentOllama /v1 当成 OpenAI 兼容模型接进来和 6/2-prog 相比,最大的差别不是“任务变了”,而是:
这份代码可以分成 5 块:
agent 和工具main()如果你对照 6/2-prog 去看,会发现这里少掉了一整大块:
urllib 请求到 /api/chatFunctionModel 适配层这正是这一版最值得理解的地方。
from __future__ import annotations这一行和前面一样,主要是为了让类型标注写起来更自然。
下面这些是标准库:
import argparse
import math
import os
import sys它们分别做什么:
argparse
math
os
sys
下面这些是 PydanticAI 相关导入:
from pydantic_ai import Agent
from pydantic_ai.models.openai import OpenAIChatModel
from pydantic_ai.providers.openai import OpenAIProvider这里最关键的两个新对象是:
OpenAIChatModelOpenAIProvider你可以先这样理解:
OpenAIChatModel
PydanticAI:我要接一个“OpenAI 风格的聊天模型”OpenAIProvider
PydanticAI:这个模型具体在哪儿Ollama /v1DEFAULT_PROMPTDEFAULT_PROMPT = (
"请分析一个 2.4 GHz 校园无线链路..."
)这一段和 6/2-prog 基本一样。
它给模型提供了:
并明确要求:
这说明一个很重要的事情:
clear_proxy_env()def clear_proxy_env() -> None:这个函数仍然保留了下来。
为什么?
因为即使现在走的是:
http://localhost:11434/v1它本质上仍然是本地网络请求。
如果电脑上开了代理,有时本地请求也会被代理干扰。
所以这里删掉了:
"http_proxy",
"https_proxy",
"HTTP_PROXY",
"HTTPS_PROXY",
"all_proxy",
"ALL_PROXY",对初学者来说,可以记住这一点:
build_agent(model_name, base_url)def build_agent(model_name: str, base_url: str) -> Agent[None, str]:这个函数的作用是:
agent输入参数有两个:
model_name
qwen3.5:0.8bbase_url
Ollama /v1 的地址返回值:
Agent这里的类型:
Agent[None, str]可以先简单理解成:
clear_proxy_env()这一步和前面讲的一样,是为了保证本地 Ollama 访问稳定。
model = OpenAIChatModel(
model_name,
provider=OpenAIProvider(
base_url=base_url,
api_key="ollama",
),
)这是整份脚本里最关键、也最“省代码”的部分。
你可以把它拆成两层看。
OpenAIProvider(...)OpenAIProvider(
base_url=base_url,
api_key="ollama",
)这里的参数:
base_url=base_url
http://localhost:11434/v1api_key="ollama"
Ollama 来说,给一个占位字符串就够了OpenAIChatModel(...)OpenAIChatModel(
model_name,
provider=...
)意思是:
qwen3.5:0.8b这几行的意义非常大,因为它意味着:
PydanticAI 已经知道怎么和这个模型说话6/2-prog 短这么多因为在 6/2-prog 里,我们要自己做这些事:
PydanticAI 的消息翻译成 Ollama /api/chat 格式tools schemaOllama 的返回再翻译回 ModelResponseFunctionModel而这一版里,这些都被:
OpenAIChatModelOpenAIProvider接管了。
也就是说:
这就是为什么行业里大家一直很在意“OpenAI 兼容接口”。
agent = Agent(
model=model,
system_prompt=(...),
output_type=str,
)这三项分别表示:
model=model
system_prompt=(...)
output_type=str
system_prompt 在这里做了什么这里写了很多句子,其实每一句都在约束 agent 的行为:
analyze_wireless_linksuggest_modulation这说明:
@agent.tool_plain当你看到:
@agent.tool_plain
def analyze_wireless_link(...):它的意思是:
这就是 PydanticAI 最适合教学的地方之一:
analyze_wireless_link(...)def analyze_wireless_link(
frequency_ghz: float,
distance_km: float,
tx_power_dbm: float,
tx_gain_dbi: float,
rx_gain_dbi: float,
other_loss_db: float,
bandwidth_mhz: float,
noise_figure_db: float,
) -> dict[str, float]:这个函数一次性完成整个链路预算的主要计算。
为什么要这样设计?
因为对小模型来说:
所以这里用一个“大一点但仍然清楚”的工具,把这些量一次算出来:
SNRpath_loss = 92.45 + 20 * math.log10(frequency_ghz) + 20 * math.log10(distance_km)这是自由空间路径损耗 FSPL 的常见形式。
含义是:
received = tx_power_dbm + tx_gain_dbi + rx_gain_dbi - path_loss - other_loss_db这就是一个简单的链路预算表达:
bandwidth_hz = bandwidth_mhz * 1_000_000因为后面的热噪声和容量公式都需要 Hz。
noise = -174 + 10 * math.log10(bandwidth_hz) + noise_figure_db这里:
-174
dBm/Hz10 * log10(bandwidth_hz)
noise_figure_db
SNRsnr_value = received - noise就是:
capacity_bps = bandwidth_hz * math.log2(1 + 10 ** (snr_value / 10))这里做了两件事:
dB 的 SNR 变成线性值10 ** (snr_value / 10)C = Blog2(1 + SNR)
return {
"path_loss_db": round(path_loss, 3),
"received_power_dbm": round(received, 3),
"noise_floor_dbm": round(noise, 3),
"snr_db": round(snr_value, 3),
"shannon_capacity_mbps": round(capacity_bps / 1_000_000, 3),
}这里有两个关键点:
round(..., 3)
capacity_bps / 1_000_000
bps 转成 Mbps所以这个工具最后返回的是一个结构化字典。
而这正是 agent 喜欢的输入:
suggest_modulation(snr_db_value)def suggest_modulation(snr_db_value: float) -> str:这个工具很简单:
SNR逻辑是分段判断:
这里的重点不是“这就是严格标准答案”,而是:
这也说明 agent 工具并不一定都很复杂:
main()parser = argparse.ArgumentParser(
description="Run a minimal PydanticAI communications agent via Ollama /v1."
)这一步是给脚本提供命令行接口。
--modelparser.add_argument(
"--model",
default="qwen3.5:0.8b",
help="Local Ollama model name, default: qwen3.5:0.8b",
)这个参数让你可以换模型名。
默认值是:
qwen3.5:0.8b--base-urlparser.add_argument(
"--base-url",
default="http://localhost:11434/v1",
help="Ollama OpenAI-compatible base URL, default: http://localhost:11434/v1",
)这个参数非常关键,因为它正是这一版和 6/2-prog 的最大区别:
/api/chat/v1也就是说,这一行其实是在告诉学生:
--promptparser.add_argument(
"--prompt",
default=DEFAULT_PROMPT,
help="User prompt for the communications agent",
)这个参数让你可以从命令行换题目。
agent = build_agent(model_name=args.model, base_url=args.base_url)
result = agent.run_sync(args.prompt)这两行就是主流程:
这里的 run_sync(...) 可以先简单理解成:
except Exception as exc:
print(f"Failed to run the agent: {exc}", file=sys.stderr)
return 1这一版的异常处理比 6/2-prog 更简单。
为什么?
因为这次很多底层网络细节已经交给:
OpenAIProviderOpenAIChatModel去处理了。
所以这也再次说明:
print(result.output)
return 0这里:
result.output
return 0
最后这一句:
if __name__ == "__main__":
raise SystemExit(main())是标准 Python 脚本入口写法。
6/2-prog 最值得对照看的地方如果你要真正学会 agent 编程,最应该比较这两版:
6/2-prog6/3-prog/v1 接口本身稳定可用所以课堂上最值得学生带走的一点是:
PydanticAI 的工具注册方式没有变/v1 一旦兼容,OpenAIProvider 会让代码明显简化这份脚本后面可以继续扩成:
这样它就能从“最小 demo”逐步长成一个更完整的 agent 系统。