这份脚本和 6/3-prog/0-pydanticai-openai-comm-agent.py 做的是同一件事:
都通过本地 Ollama /v1 调 qwen 模型,解决一个通信领域的小问题。
区别在于:
6/3-prog 更像“单 agent + 工具调用”所以这份代码更适合讲清楚 CrewAI 的几个核心词:
AgentTaskCrewToolProcess你可以把它理解成一个两步工作流:
SNR 做调制建议最后把两个阶段的结果合成一份中文结论,并写到本目录下的 Markdown 报告里。
脚本开头先导入:
argparsejsonmathoswarningsPathpydanticcrewai这里有两个很实用的小动作:
warnings.filterwarnings(...)它的作用是把当前环境里那个 logfire-plugin 的导入警告压掉。
这个警告不影响 demo 运行,但会把终端刷得很乱。
os.environ.setdefault("OTEL_SDK_DISABLED", "true")这行是为了减少 tracing / telemetry 相关的干扰。
对课堂 demo 来说,我们只想把例子跑通,不想让同学一上来就被 tracing 配置打断。
DEFAULT_PROMPT 和 MODEL_PRESETSDEFAULT_PROMPT这是默认题目。
内容和 6/3-prog 保持一致,还是那道无线链路分析题。
这样做的好处是:
PydanticAI 和 CrewAIMODEL_PRESETS这里定义了两个模型预设:
small -> qwen3.5:0.8bfull -> qwen3:4b这相当于一个方便的开关。
如果小模型卡住,就可以直接切到 full。
clear_proxy_env()这个函数会清掉:
http_proxyhttps_proxyall_proxy以及对应的大写版本。
为什么要这样做?
因为很多同学电脑里装过代理。
一旦这些环境变量存在,本来应该访问本地 http://localhost:11434/v1 的请求,就可能被错误地送到代理上去。
所以这一步的本质是:
Ollamaclass LinkAnalysisArgs(BaseModel)这是第一个工具的参数定义。
它列出了链路预算要用到的全部输入:
frequency_ghzdistance_kmtx_power_dbmtx_gain_dbirx_gain_dbiother_loss_dbbandwidth_mhznoise_figure_db这里用 Pydantic 的意义是:
class ModulationArgs(BaseModel)这是第二个工具的参数定义。
它非常简单,只需要一个:
snr_db_value因为第二个工具只负责把 SNR 变成调制建议。
AnalyzeWirelessLinkTool这一类继承自 BaseTool。
namename: str = "analyze_wireless_link"这是工具名。
模型调用工具时,看到的就是这个名字。
descriptiondescription: str = "计算无线链路的路径损耗、接收功率、噪声底、SNR 和 Shannon 容量。"这是给模型看的工具说明。
说明写得越清楚,模型越容易正确选工具。
args_schemaargs_schema: type[BaseModel] = LinkAnalysisArgs这表示:
_run(...)这是工具真正执行的函数。
它逐行做了这些事:
path_loss = 92.45 + ...这是自由空间路径损耗的近似公式。
作用:
received = ...这是接收功率的链路预算公式。
它把:
放在一起,得到最终的接收功率。
bandwidth_hz = bandwidth_mhz * 1_000_000把 MHz 转成 Hz。
因为后面 Shannon 容量公式要用 Hz。
noise = -174 + 10 * math.log10(bandwidth_hz) + noise_figure_db这是热噪声底的常见写法。
含义是:
-174 dBm/Hz 是热噪声基准10log10(B) 是带宽放大后的噪声snr_value = received - noise接收功率减噪声底,就是 SNR。
capacity_bps = bandwidth_hz * math.log2(1 + 10 ** (snr_value / 10))这是 Shannon 容量公式。
注意这里的 SNR 要先从 dB 转成线性值,所以有:
10 ** (snr_value / 10)最后返回的是一个字典。
这样后面的 agent 就能直接读这些字段。
SuggestModulationTool这个工具更简单。
它的输入只有一个:
snr_db_value然后按区间给出建议:
BPSKQPSK16QAM 可以尝试64QAM 可以考虑64QAM 通常可行这不是严谨的系统设计结论,而是:
也正因为它简单,所以特别适合拿来说明:
build_llm()return LLM(
model=f"openai/{model_name}",
base_url=base_url,
api_key="ollama",
temperature=0,
)这一段是整个 6/4-prog 和 6/3-prog 最接近的地方。
它表示:
CrewAI 底层通过 LiteLLMOllama /v1 当成 OpenAI-compatible 接口来用model=f"openai/{model_name}"?因为在这条接法里:
openaiOllamaapi_key="ollama"?因为 OpenAI-compatible 接口通常要求带一个 key 字段。
对本地 Ollama 来说,这里只是占位值。
temperature=0?为了让课堂演示更稳定。
这样每次输出不会飘得太厉害。
build_crew()这是这份脚本最核心的部分。
analysis_agent它的任务是:
analyze_wireless_link所以它只挂了一个工具:
AnalyzeWirelessLinkTool()advisor_agent它的任务是:
snr_dbsuggest_modulation所以它只挂了一个工具:
SuggestModulationTool()这就是 CrewAI 的一个重要特点:
analysis_task这个任务明确要求:
analyze_wireless_link这一步是在强行把第一位 agent 的职责收紧。
不让它一边算、一边解释、一边建议。
advice_task这个任务有一行很关键:
context=[analysis_task]这表示:
所以 advisor_agent 不需要重新算链路预算,
它只需要读前面的结果,再做调制建议。
这就是 CrewAI 里“任务串起来”的最基本方法。
Crew(...)return Crew(
agents=[analysis_agent, advisor_agent],
tasks=[analysis_task, advice_task],
process=Process.sequential,
verbose=False,
tracing=False,
)这里最重要的是:
agents=[...]表示这套系统里有哪些 agent。
tasks=[...]表示要执行哪些任务。
process=Process.sequential表示:
先做第一步,再做第二步。
这和前面 6/3-prog 的区别非常明显:
6/3-prog 主要是“一个 agent 调两个工具”summarize_task_output(task_output)这个函数把每个任务输出整理成一个更容易保存的字典。
里面保留了:
write_markdown_report(...)这个函数会把运行结果写到:
0-crewai-openai-comm-agent-output.md里面包含:
这和我们前面几套 demo 一样,目的都是:
main()这里主要做三件事:
--model-preset这是你前面刚加过的模型开关:
smallfullcrew.kickoff(inputs={"user_prompt": args.prompt})这行表示真正开始执行 crew。
同时把用户题目传进 task 模板里的 {user_prompt}。
print(result.raw)把最终输出先打印到终端。
write_markdown_report(...)把结果再写到文件。
如果你是编程初学者,看完这份脚本,最该抓住的是四件事:
Tool 其实就是“给模型调用的 Python 能力”Agent 是“谁来做这件事”Task 是“这一步到底要完成什么”Crew 是“把多步任务按顺序串起来”如果你把这四层关系看明白了,再回去看:
6/2-prog6/3-prog6/4-prog你就能很清楚地区分: