AI Agent 开发预备知识:从 Python 异步到流式通信
AI Agent 开发预备知识:从 Python 异步到流式通信
⚠️ 重要:本文包含 Python 异步编程、类型系统与 Pydantic v2、环境工程(uv/pip/API Key 管理)、HTTP 与 SSE 流式协议 等 AI Agent 开发四大预备知识。
文中涉及的相关代码示例地址:https://github.com/m12305/Langchain-LangGraph-agent — Langchain/LangGraph 学习项目
相关项目推荐:https://github.com/m12305/hello-FastAPI — FastAPI 学习项目
在动手写第一个 LangGraph Agent 之前,有四块基石必须先铺好:异步编程、类型系统、环境工程、HTTP 与流式协议。本文将这四大预备知识串联成一幅完整的地图。
文章目录
为什么需要这些预备知识?
当我们用 LangChain/LangGraph 构建 Agent 时,代码长这样:
from typing import Annotated, TypedDict
import asyncio, operator
class AgentState(TypedDict):
messages: Annotated[list[str], operator.add]
async for event in app.astream_events( # ← 异步 + 流式
{"messages": [{"role": "user", "content": "帮我查天气"}]},
version="v2"
):
if event["event"] == "on_tool_start":
print(f"🔧 正在调用工具: {event['name']}")
短短几行代码,涉及了 异步编程 (async for)、类型系统 (TypedDict, Annotated)、环境配置 (API Key 管理)、HTTP/SSE (astream_events 底层协议)。如果你的基础不牢,这段代码就是天书。这篇文章帮你逐个击破。
一、Python 异步编程:让 Agent “同时做多件事”
1.1 一句话理解同步 vs 异步
- 同步:排队办事。冲咖啡要 3 分钟,烤面包要 2 分钟,你不等咖啡冲完就不去烤面包,总共花 5 分钟。
- 异步:同时开工。咖啡机和烤面包机同时运转,谁先好先处理谁,总共花 3 分钟。
import asyncio
# 同步:总耗时 = 3s + 2s = 5s
def sync_breakfast():
make_coffee() # 阻塞 3 秒
make_toast() # 阻塞 2 秒
# 异步:总耗时 ≈ max(3s, 2s) = 3s
async def async_breakfast():
await asyncio.gather(
make_coffee_async(), # 同时执行
make_toast_async(), # 同时执行
)
1.2 核心概念三件套
| 概念 | 一句话解释 | 对应 Python 语法 |
|---|---|---|
| 协程 | 用 async def 定义的"可暂停函数" |
async def foo(): ... |
| 事件循环 | 调度中心,不停轮询"谁可以继续往下走了" | asyncio.run(main()) |
| Task | 协程的包装,提交给事件循环后可并发调度 | asyncio.create_task() |
🔑 最重要的直觉:
await不是"死等",而是"让出控制权"——告诉事件循环:“这件事需要点时间,你先去处理别的,好了叫我。”
1.3 在 Agent 中的高频用法
# 1. 并发调用多个 LLM
results = await asyncio.gather(
call_gpt4(prompt),
call_claude(prompt),
call_gemini(prompt),
return_exceptions=True # 某个挂了不影响其他
)
# 2. 超时降级
try:
result = await asyncio.wait_for(call_llm(prompt), timeout=2.0)
except asyncio.TimeoutError:
result = "使用缓存结果..."
# 3. 流式消费(LangGraph 核心)
async for chunk in llm.astream(prompt):
print(chunk.content, end="", flush=True)
1.4 常见坑速查
| ❌ 错误 | ✅ 正确 |
|---|---|
协程里用 time.sleep() |
协程里用 await asyncio.sleep() |
协程里用 requests.get() |
协程里用 httpx.AsyncClient |
await 写在普通函数里 |
普通函数调用协程用 asyncio.run() |
忘写 await |
开启 mypy 类型检查 |
二、Python 类型系统:让 IDE 帮你写代码
2.1 为什么 Agent 开发特别需要类型?
Agent 的 State 是贯穿所有节点的"灵魂结构"。没有类型:
def agent_node(state):
# state 里有什么字段?全是猜!
msg = state["message"] # 拼写错误,运行时才炸!
有了类型:
class AgentState(TypedDict):
messages: list[str] # ← IDE 自动补全
user_id: str # ← 拼写错误当场红色波浪线
turn_count: int # ← 用错类型当场报错
2.2 TypedDict vs Pydantic:两大 State 定义方式
TypedDict——轻量级,零依赖:
from typing import Annotated, TypedDict
import operator
class AgentState(TypedDict):
# Annotated[类型, 合并函数] —— LangGraph 的精髓
messages: Annotated[list[str], operator.add] # 追加而非覆盖!
current_tool: str | None # 没有 Annotated → 直接覆盖
turn_count: int
Pydantic v2——重量级,带验证:
from pydantic import BaseModel, Field
class AgentState(BaseModel):
messages: list[str] = Field(default_factory=list)
user_id: str
temperature: float = Field(default=0.7, ge=0.0, le=2.0)
class Config:
extra = "forbid" # 严格模式
💡 选型建议:简单场景用 TypedDict,需要复杂的验证(如工具输入参数定义)用 Pydantic。
2.3 Pydantic 不止做 State——工具定义 & 结构化输出
# 1. 定义工具输入 Schema
class SearchInput(BaseModel):
query: str = Field(description="搜索关键词")
max_results: int = Field(default=5, ge=1, le=20)
# 2. 定义 LLM 的结构化输出
class SentimentResult(BaseModel):
sentiment: Literal["positive", "negative", "neutral"]
confidence: float = Field(ge=0.0, le=1.0)
keywords: list[str]
# chain = prompt | llm.with_structured_output(SentimentResult)
三、环境工程:别把 API Key 提交到 GitHub
3.1 包管理:为什么选 uv?
| 工具 | 速度 | 推荐度 | 理由 |
|---|---|---|---|
pip |
慢 | ⭐⭐ | 简单,适合快速原型 |
poetry |
中 | ⭐⭐⭐ | 传统项目首选 |
uv |
极快 | ⭐⭐⭐⭐ | AI 项目首选,Rust 实现 |
# 一行初始化
uv init my-agent && cd my-agent
uv add langchain langchain-openai langgraph
uv run python script.py
3.2 API Key 管理的铁律
❌ 永远不要硬编码 API Key
❌ 永远不要提交 .env 到 Git
✅ 使用 python-dotenv 加载环境变量
✅ 提供 .env.example 作为模板
✅ 企业级方案:Pydantic Settings
# 标准加载方式
from dotenv import load_dotenv
import os
load_dotenv()
# 启动时校验,避免跑到一半才发现 Key 缺失
for key in ["OPENAI_API_KEY", "LANGSMITH_API_KEY"]:
if not os.getenv(key):
raise SystemExit(f"缺少环境变量: {key}")
3.3 强烈建议:从第一天起就配好 LangSmith
# .env 中添加
LANGSMITH_API_KEY=lsv2_xxx
LANGSMITH_PROJECT=my-agent
LANGSMITH_TRACING=true
配置后每次 LangChain 调用都会自动上报 Trace 到 LangSmith 后台,你可以可视化地看到 Agent 的调用链——这对调试 Agent 的决策过程至关重要。
四、HTTP 与流式基础:LLM 通信的底层真相
4.1 所有 LLM API 本质上都是一个 HTTP POST
# LangChain 的 invoke() 底层就是这玩意儿
response = requests.post(
"https://api.openai.com/v1/chat/completions",
headers={"Authorization": "Bearer sk-xxx"},
json={"model": "gpt-4o", "messages": [...], "stream": False},
)
data = response.json()
print(data["choices"][0]["message"]["content"])
LangChain 做的,就是把这些原生 HTTP 调用封装成优雅的 llm.invoke("你好")。
4.2 流式 vs 非流式:用户体验的分水岭
非流式 (stream=False):
Client ──→ POST ──→ Server
←── 等待 5 秒,盯白屏... ←──
←── 一次性返回完整内容
流式 (stream=True, SSE):
Client ──→ POST (stream=True) ──→ Server
←── data: "床" ← 0.1s
←── data: "前" ← 0.2s
←── data: "明" ← 0.3s
←── data: "月" ← 0.4s
←── data: "光" ← 0.5s
←── data: [DONE]
非流式:用户盯着空白屏幕干等,体验差。流式:0.1 秒就看到第一个字,体感延迟极低。
4.3 为什么 LLM 流式用 SSE 而不 WebSocket?
| 协议 | 方向 | 为什么选/不选 |
|---|---|---|
| SSE ⭐ | 服务器→客户端,单向 | ✅ 方向匹配 LLM 生成模式 |
| WebSocket | 双向 | ❌ 杀鸡用牛刀,LLM 不需要双向 |
| SSE 优势: | 普通 HTTP 即可,不需要协议升级 |
LLM 生成文本是典型的"服务器往客户端单向推数据"场景——SSE 最合适。
4.4 Agent 的流式本质是"决策流"
普通的聊天流式只是逐 token 出字,而 Agent 的流式是"决策的曝光":
🤔 Agent 正在分析用户请求...
🔧 决定调用工具: search_knowledge_base
📋 参数: {"query": "退款政策", "top_k": 5}
✅ 检索到 5 篇相关文档
💬 正在生成回答: 根据您的订单记录...
# LangGraph 中捕获这些决策事件
async for event in app.astream_events(input, version="v2"):
kind = event["event"]
if kind == "on_chat_model_stream":
print(event["data"]["chunk"].content, end="") # 逐 token
elif kind == "on_tool_start":
print(f"\n🔧 调用工具: {event['name']}") # 工具调用
elif kind == "on_tool_end":
print(f"\n✅ 工具返回: ...") # 工具结果
4.5 错误处理:429 和 500 要区别对待
from tenacity import retry, stop_after_attempt, wait_exponential
@retry(
stop=stop_after_attempt(3),
wait=wait_exponential(multiplier=1, min=2, max=30), # 2s → 4s → 8s
)
async def call_llm_with_retry(prompt: str):
async with httpx.AsyncClient(timeout=30) as client:
response = await client.post(url, json=payload)
if response.status_code == 429: # 速率限制 → 重试
raise
elif response.status_code >= 500: # 服务器错误 → 重试
raise
response.raise_for_status()
return response.json()
五、四块基石如何拼成完整地图?
来一个全景视角——当你写下 async for event in app.astream_events(...) 时,背后发生了什么:
┌────────────────────────────────────────────────────────┐
│ LangGraph Agent 执行全景 │
├────────────────────────────────────────────────────────┤
│ │
│ 环境工程 (.env) │
│ └─→ API Key 管理 ──→ 安全地连上 LLM Provider │
│ │
│ 异步编程 (asyncio) │
│ └─→ astream_events() ──→ 不阻塞地消费事件流 │
│ └─→ asyncio.gather() ──→ 并发调用多个工具 │
│ │
│ 类型系统 (TypedDict/Pydantic) │
│ └─→ AgentState ──→ 所有节点共享同一份"记忆" │
│ └─→ Annotated[list, add] ──→ 消息追加而非覆盖 │
│ └─→ Pydantic Model ──→ 工具参数自动校验 │
│ │
│ HTTP/SSE (httpx) │
│ └─→ POST /chat/completions ──→ 每次 LLM 调用 │
│ └─→ SSE stream ──→ 逐 token 接收响应 │
│ └─→ astream_events ──→ 转化为"决策流"事件 │
│ │
└────────────────────────────────────────────────────────┘
总结
预备知识往往是最容易被跳过的部分——但它决定了你后面是"读懂代码"还是"背住代码"。
| 预备知识 | 一句话总结 | 在 Agent 中最直接的体现 |
|---|---|---|
| Python 异步 | await 是让出控制权,不是死等 |
ainvoke(), astream_events() |
| 类型系统 | 类型让 IDE 帮你看代码,而不是你帮 IDE 看代码 | AgentState, Annotated |
| 环境工程 | API Key 永远不裸奔,.env 不进 Git |
load_dotenv(), LangSmith |
| HTTP/SSE | LLM 调用的本质是一个 HTTP POST + SSE 流 | stream=True, astream_events |
四块基石就位,下一站——从零开始构建你的第一个 LangGraph Agent 🚀
本文基于"AI Agent 学习项目"第 0 阶段(预备知识)整理,覆盖 0.1 Python 异步编程、0.2 Python 类型系统、0.3 环境工程、0.4 HTTP 与流式基础 四个章节。*
更多推荐



所有评论(0)