当前位置: 首页 > news >正文

Pydantic AI构建生产级LLM流式响应系统

1. 项目概述:用 Pydantic AI 构建可维护、可扩展的 LLM 流式响应系统

你有没有遇到过这样的场景:前端页面上,用户刚敲完问题,光标还在闪烁,答案就一行行“打字机式”地浮现出来——不是等几秒后整段弹出,而是实时、连贯、有呼吸感地流出来。这种体验背后,是 LLM 流式(streaming)能力在起作用。但真正把它从“能跑通”变成“能上线、能监控、能加工具、能换模型、能写测试”的生产级模式,中间隔着一堵墙。这堵墙,不是技术原理,而是工程实践:怎么组织代码结构?怎么统一错误处理?怎么把工具调用(function calling)和流式输出自然融合?怎么让新同事三天内就能读懂、修改、加功能?这篇文章讲的就是我踩了至少七次坑之后,用 Pydantic AI 拆掉这堵墙的过程。

核心关键词很明确:Streaming(流式响应)、Pydantic AI(LLM Agent 框架)、Tool Calling(工具调用)、Productionization(生产化)。它不是教你如何调用一个 API,而是帮你搭建一套“骨架”——以后换 Anthropic 到 OpenAI,加数据库查询工具,接入企业知识库插件,甚至做多步推理链,都不需要推翻重写。我试过纯手写 async/await + event.type 判断的方案,也用过 LangChain 的 RunnableStream,最后选 Pydantic AI,不是因为它最火,而是它最“轻量却完整”:它不试图包揽一切(比如不内置向量库或记忆管理),但把 LLM 交互中最关键的三件事——输入建模、输出解析、流式事件编排——用 Pydantic 的强类型哲学,焊死在了一起。你写的每一个 message、每一个 tool call、每一个 streaming chunk,都有明确的 Python 类型定义,IDE 能自动补全,mypy 能静态检查,单元测试能直接 mock 输入输出。这不是炫技,是每天少 debug 两小时的真实收益。适合谁?如果你正在用 FastAPI 做后端、用 Streamlit 做内部工具、或者正为一个需要“实时反馈+外部系统联动”的 AI 功能发愁,那这篇就是为你写的。它不假设你懂 agent 架构,但要求你熟悉 Python 异步基础和 Pydantic 的基本用法——这两点,我都会在后续步骤里用实操带过。

2. 整体设计思路与框架选型逻辑

2.1 为什么不是 LangChain 或 LlamaIndex?

先说清楚“不选什么”和“为什么”。LangChain 是生态最全的,但它像一辆功能齐全的 SUV:备胎、车载冰箱、全景天窗一应俱全,但你要只是每天通勤 5 公里,启动它、预热它、找钥匙孔的时间,可能比走路还长。它的 Runnable 接口抽象层深,stream()方法返回的是RunnableStream对象,底层封装了大量状态管理逻辑。当你想精确控制某一次content_block_delta的 yield 时机,或者想在tool_use事件触发后立刻执行一段同步 DB 查询再把结果塞回流里,LangChain 的 hook 链路会变得非常绕,调试时得一层层扒源码。LlamaIndex 更侧重 RAG 场景,它的 streaming 主要服务于检索-生成流水线,对通用 tool calling 的支持是作为插件存在的,类型安全弱,错误提示模糊(比如传错 tool name,它可能只报KeyError,而不是告诉你“你声明的 tool schema 里没有这个函数”)。

2.2 为什么是 Pydantic AI?三个不可替代的支点

Pydantic AI 的核心价值,在于它把 LLM 交互这件事,彻底“Python 化”和“类型化”了。它不是造一个新的 DSL(领域特定语言),而是用你 already know 的东西——Pydantic Models、Python type hints、async generators——来构建 agent。具体看三个支点:

第一支点:输入即模型(Input as Model)
在 Pydantic AI 里,你不是拼接一个 dict 或 list 给client.messages.create(),而是定义一个继承自BaseModel的类,比如DAXQueryRequest

from pydantic import BaseModel, Field from typing import List, Optional class DAXQueryRequest(BaseModel): user_question: str = Field(..., description="用户提出的自然语言问题") context_tables: List[str] = Field(..., description="当前可用的数据表名列表") preferred_language: str = Field(default="zh", description="期望返回的语言")

这个类本身就能做三件事:1)自动校验用户输入是否合法(比如context_tables是不是空列表);2)生成清晰的 system prompt 描述(model_json_schema()可导出 JSON Schema 供 LLM 理解);3)作为文档,新成员看一眼就知道接口契约。而 LangChain 的input是个泛型Any,靠 docstring 和 runtime 断言,脆弱得多。

第二支点:输出即解析(Output as Parse)
流式响应最难的,不是“怎么 yield”,而是“怎么确保 yield 出来的每一块,都是你预期的结构”。Pydantic AI 的stream_structured()方法,会把整个流式事件流,按你定义的输出模型(比如DAXQueryResponse)自动组装、校验、解析。它内部监听content_block_deltatool_usemessage_stop等事件,当检测到一个完整的 tool call 时,会暂停 yield 文本,转而解析tool_use的参数,然后——关键来了——它会把这个解析后的ToolCall对象,直接传给你注册的tool函数。你不用自己写if event.type == 'tool_use': parse_tool_call(event)这种胶水代码。这省下的不是几行代码,是避免 80% 的流式 tool calling bug 的根本保障。

第三支点:流即生成器(Stream as Generator)
Pydantic AI 的stream()返回的是标准AsyncGenerator[Union[str, ToolCall], None]。这意味着你可以用最原生的 Python 语法去消费它:

async for chunk in agent.stream(request): if isinstance(chunk, str): yield f"data: {json.dumps({'text': chunk})}\n\n" elif isinstance(chunk, ToolCall): # 这里直接拿到已解析好的工具调用对象 result = await execute_tool(chunk) yield f"data: {json.dumps({'tool_result': result})}\n\n"

没有中间抽象层,没有隐藏状态,没有你无法控制的缓冲区。你想在 yield 前加日志、加性能计时、加敏感词过滤,一行print(f"Yielding chunk: {chunk[:50]}")就搞定。这种透明性,是生产环境 debug 的生命线。

提示:Pydantic AI 的“小”是战略性的。它不提供MemoryRetrieverCallbackHandler这些模块,因为这些本该由你的业务框架决定。你用 Redis 做 memory?自己写个RedisMemoryBackend实现它的Memory协议就行。你要集成 Sentry 做错误追踪?在agent.stream()外层包个 try/except,手动 send event。它强迫你思考“什么该由框架管,什么该由我管”,这恰恰是长期维护成本最低的路径。

3. 核心细节解析与实操要点

3.1 环境准备与依赖锁定

别跳过这一步。Pydantic AI 发展很快,0.1.x 和 0.2.x 的 API 差异不小(比如stream_structured在 0.2.0 后才成为默认行为)。我用的是Pydantic AI 0.2.4,配合Anthropic 0.39.0(注意:不是anthropic,是anthropic的新版,旧版叫anthropic-sdk,已废弃)。pydantic必须是v2.7+,因为 Pydantic AI 重度依赖 v2 的@model_validatorRootModel。我的requirements.txt是这样锁死的:

pydantic==2.7.1 pydantic-ai==0.2.4 anthropic==0.39.0 httpx==0.27.0 # Pydantic AI 底层用 httpx,版本不匹配会导致 stream 断连

为什么强调httpx?因为我在测试中发现,httpx==0.26.0下,当流式响应中夹杂大量tool_use事件时,httpx.AsyncClient会偶发性地提前关闭连接,导致streamgenerator 抛httpx.ReadTimeout。升级到0.27.0后,这个问题消失。这不是玄学,是 httpx 内部对 HTTP/2 流控逻辑的修复。所以,永远不要用pip install pydantic-ai这种裸装方式,一定要 pin 版本。

3.2 定义你的第一个 Agent:从零开始的 DAX 查询助手

我们以原文提到的“DAX LLM”为原型,构建一个能理解用户自然语言、并调用 Power BI DAX 引擎执行查询的助手。核心是两个模型:输入请求模型和输出响应模型。

输入模型(DAXQueryRequest)

from pydantic import BaseModel, Field from typing import List, Optional, Literal class DAXQueryRequest(BaseModel): user_question: str = Field( ..., description="用户用中文或英文提出的关于数据的问题,例如'上个月销售额最高的产品是什么?'" ) available_tables: List[str] = Field( ..., description="当前 Power BI 数据模型中可用的表名列表,例如['Sales', 'Products', 'Customers']" ) time_range: Optional[str] = Field( default=None, description="可选的时间范围描述,如'last_month'、'Q1_2024',用于指导 DAX 时间智能函数" ) output_format: Literal["table", "chart", "summary"] = Field( default="table", description="用户期望的输出格式" )

这个模型的关键在于description字段。Pydantic AI 会把这些 description 自动注入 system prompt,告诉 LLM:“你必须严格根据以下字段描述来理解用户意图”。这比你在代码里硬编码一段 system prompt 字符串,要可靠得多——因为 model 的变更会强制你更新 description,不会出现“代码改了但 prompt 没改”的经典事故。

输出模型(DAXQueryResponse)

from pydantic import BaseModel, Field from typing import List, Dict, Any, Optional class DAXResultRow(BaseModel): """DAX 查询返回的单行结果""" columns: Dict[str, Any] = Field(..., description="列名到值的映射,例如 {'Product': 'iPhone 15', 'SalesAmount': 125000}") class DAXQueryResponse(BaseModel): """LLM 生成的最终响应,包含 DAX 语句和预期结果结构""" dax_statement: str = Field( ..., description="生成的、可直接在 Power BI 中执行的 DAX 查询语句" ) expected_columns: List[str] = Field( ..., description="该 DAX 语句预期返回的列名列表,用于前端渲染表格" ) reasoning: str = Field( ..., description="用中文简要解释为什么生成这个 DAX 语句,帮助用户理解" ) # 注意:这里没有定义 'result_rows',因为实际结果由 tool call 返回,不是 LLM 直接生成的

这个模型的设计意图很明确:它只负责“规划”(Planning),不负责“执行”(Execution)。LLM 的任务是生成正确的 DAX,而执行 DAX 并返回真实数据,是后面execute_dax_querytool 的事。这种分离,是构建可靠 agent 的基石。如果让 LLM 既生成 DAX 又“幻觉”出结果,那错误就永远无法定位——是 DAX 写错了?还是 LLM 编造了数据?分开了,debug 就变成了两个独立、可验证的步骤。

3.3 Tool Call 的注册与执行:让 LLM 真正“动手”

Pydantic AI 的 tool 不是字符串函数名,而是一个实现了Tool协议的 Python 函数。它必须:

  • 接收一个ToolCall对象(已解析好参数)
  • 返回一个ToolResult对象(可以是任意 JSON-serializable 类型)
  • async的(因为执行 DAX 查询必然是异步 IO)

我们来写execute_dax_query

import asyncio from pydantic_ai import Tool, ToolResult from typing import Dict, Any # 假设你有一个异步的 DAX 执行客户端 class DAXClient: async def execute(self, dax_statement: str) -> Dict[str, Any]: # 这里是伪代码,实际对接 Power BI XMLA endpoint 或其他 DAX 引擎 await asyncio.sleep(0.5) # 模拟网络延迟 return { "status": "success", "rows": [ {"Product": "iPhone 15", "SalesAmount": 125000}, {"Product": "MacBook Pro", "SalesAmount": 98000} ], "columns": ["Product", "SalesAmount"] } dax_client = DAXClient() @Tool() async def execute_dax_query(dax_statement: str) -> ToolResult: """ 执行 DAX 查询语句,并返回结构化结果。 Args: dax_statement: 要执行的 DAX 语句,例如 "EVALUATE TOPN(1, Sales, Sales[Amount], DESC)" Returns: ToolResult: 包含 status, rows, columns 的字典 """ try: result = await dax_client.execute(dax_statement) return ToolResult(result) except Exception as e: return ToolResult({"status": "error", "message": str(e)})

注册这个 tool 时,Pydantic AI 会自动读取它的__doc__和参数注解,生成符合 Anthropic 工具规范的tools数组。你完全不需要手动构造那个嵌套的 JSON Schema。这就是“类型即文档”的力量。

注意:ToolResult的类型必须是 JSON-serializable。如果你返回了一个datetime对象,Pydantic AI 会在序列化时抛错。解决方案是:在ToolResult里只放str,int,float,list,dict,bool,None,或者用json.dumps()预处理。我吃过亏——曾经返回了一个pandas.DataFrame,结果 agent 直接卡死在json.dumps()的无限递归里。

4. 实操过程与核心环节实现

4.1 构建 Streaming Agent:从 request 到 stream 的完整链路

现在,把前面定义的 Request、Response、Tool 全部组装起来,创建一个真正的 streaming agent。核心是Agent类的初始化:

from pydantic_ai import Agent, RunContext from anthropic import AsyncAnthropic from typing import AsyncGenerator, Union # 初始化 Anthropic 客户端 anthropic_client = AsyncAnthropic(api_key=ANTHROPIC_API_KEY) # 创建 Agent 实例 dax_agent = Agent( model="claude-3-5-sonnet-20241022", client=anthropic_client, # 这里注册我们定义的 tool tools=[execute_dax_query], # 这是关键!指定输出模型,让 LLM 知道它要生成什么结构 result_type=DAXQueryResponse, # 设置超时和重试,生产环境必备 max_retries=2, timeout=30.0, )

这个dax_agent实例,就是你的“流式引擎”。接下来,写一个stream_dax_response函数,它接收DAXQueryRequest,返回一个AsyncGenerator

async def stream_dax_response( request: DAXQueryRequest ) -> AsyncGenerator[Union[str, Dict[str, Any]], None]: """ 流式响应 DAX 查询请求。 Yields: str: LLM 生成的文本片段(如 reasoning 的一部分) Dict: Tool 执行结果(如 DAX 查询的真实数据) """ # Step 1: 将 Pydantic 模型转换为 Agent 可识别的输入 # Pydantic AI 期望一个 dict,key 是 model field name,value 是值 input_dict = request.model_dump() # Step 2: 调用 agent.stream(),传入输入字典 # 注意:这里用的是 .stream(),不是 .run()。.run() 是非流式,等全部完成才返回 async for chunk in dax_agent.stream(input_dict): # chunk 的类型是 Union[str, ToolResult, DAXQueryResponse] if isinstance(chunk, str): # 这是 LLM 生成的文本流,比如 reasoning 的逐字输出 yield chunk elif isinstance(chunk, ToolResult): # 这是 tool 执行后的结果 # 我们把它包装成一个标准的 JSON event,前端好解析 yield { "event": "tool_result", "data": chunk.value # ToolResult.value 就是我们 return 的内容 } elif isinstance(chunk, DAXQueryResponse): # 这是 LLM 最终生成的完整响应模型 # 通常出现在流的末尾,表示“规划完成” yield { "event": "final_response", "data": chunk.model_dump() }

这个函数就是你后端 API 的心脏。它把复杂的 LLM 交互、tool 调用、流式事件混合,封装成了一个干净的AsyncGenerator。前端(比如一个 FastAPI 的 SSE endpoint)只需要async for chunk in stream_dax_response(request):,然后把每个chunktext/event-stream格式发出去即可。

4.2 FastAPI SSE Endpoint:让前端真正“看到”流

一个典型的 FastAPI 流式 endpoint 长这样:

from fastapi import APIRouter, Depends, HTTPException, Request from fastapi.responses import StreamingResponse from starlette.concurrency import run_in_threadpool router = APIRouter() @router.post("/api/dax/stream") async def stream_dax_query( request: DAXQueryRequest, # 从依赖注入获取 agent 实例(推荐用 FastAPI 的 dependency injection) agent: Agent = Depends(get_dax_agent), ): """ SSE endpoint for streaming DAX query responses. """ async def event_generator(): try: # 调用我们上面写的 stream_dax_response async for chunk in stream_dax_response(request): if isinstance(chunk, str): # 文本流:发送 data: xxx yield f"data: {json.dumps({'type': 'text', 'content': chunk})}\n\n" elif isinstance(chunk, dict) and chunk.get("event") == "tool_result": # 工具结果:发送 data: {...} yield f"data: {json.dumps({'type': 'tool_result', 'data': chunk['data']})}\n\n" elif isinstance(chunk, dict) and chunk.get("event") == "final_response": # 最终响应:发送 data: {...} yield f"data: {json.dumps({'type': 'final', 'data': chunk['data']})}\n\n" except Exception as e: # 任何异常,都发送 error 事件 yield f"event: error\ndata: {json.dumps({'message': str(e)})}\n\n" return StreamingResponse( event_generator(), media_type="text/event-stream", headers={ "Cache-Control": "no-cache", "Connection": "keep-alive", } )

这个 endpoint 的精妙之处在于,它把stream_dax_response的所有复杂性都屏蔽掉了。前端 JavaScript 只需要:

const eventSource = new EventSource("/api/dax/stream"); eventSource.onmessage = (event) => { const data = JSON.parse(event.data); if (data.type === 'text') { appendToChat(data.content); // 逐字追加 } else if (data.type === 'tool_result') { showLoadingSpinner(false); displayTable(data.data.rows); // 显示真实数据 } };

你看,前端完全不需要知道“tool call”是什么,它只认type字段。这种前后端契约的清晰,是 Pydantic AI 带来的最大工程红利。

4.3 关键参数调优与性能实测

流式体验好不好,70% 取决于参数。我做了三轮压测(本地localhostclaude-3-5-sonnet),记录了不同配置下的首字节时间(TTFB)和总耗时:

参数配置TTFB (ms)总耗时 (s)用户感知
temperature=0.0, max_tokens=10248504.2文本生成快,但 tool call 机会少,常需重试
temperature=0.2, max_tokens=204811005.8平衡点,reasoning 充分,tool call 稳定
temperature=0.5, max_tokens=409614008.1生成更“自由”,但 DAX 语句错误率上升 12%

结论很明确:temperature=0.2是黄金值。它给了 LLM 一点创造性去理解模糊问题,又足够克制,保证 DAX 语法的严谨性。max_tokens设为2048而不是4096,是因为我们的DAXQueryResponse模型本身不长,过大的 token 限制会让 LLM 在无关的 padding 上浪费算力,反而拖慢首字节。

另一个重要参数是system_prompt。Pydantic AI 允许你传入自定义 system prompt,但我强烈建议不要覆盖它自动生成的 prompt。它的 prompt 里包含了对DAXQueryResponse模型字段的精确描述,以及对execute_dax_querytool 的调用说明。如果你自己写一个"You are a helpful DAX assistant",那 LLM 就不知道该生成dax_statement还是sql_statement。实测下来,用默认 prompt,tool call 的准确率是 92%,而自定义简单 prompt 后,掉到了 68%。

实操心得:在Agent初始化时,加一个log_level="DEBUG"参数,它会把每次 LLM 的输入 prompt、输出的 raw text、解析后的ToolCall对象,都打印到日志里。这是 debug tool call 失败的唯一可靠方法。我曾经卡在一个 bug 里两天,最后打开 debug 日志,发现 LLM 生成的tool_usename 是execute_dax_query_v2,而我注册的 tool 名是execute_dax_query——名字不一致,Pydantic AI 直接忽略,静默失败。没有日志,你永远找不到。

5. 常见问题与排查技巧实录

5.1 流式中断:Connection Reset / Incomplete Chunk

现象:前端收到前 3 行文本,然后eventsource自动关闭,控制台报Network ErrorFailed to load resource

排查路径

  1. 先看后端日志:是否有httpx.ReadTimeoutConnection closed?如果有,回到 3.1 节,确认httpx版本是0.27.0+
  2. 检查 FastAPI 的StreamingResponse:是否在event_generator()函数里,有未捕获的Exception?比如dax_client.execute()抛了TimeoutError,但你没在stream_dax_response里 try/catch。这会导致 generator 异常退出,SSE 连接立即断开。必须event_generator()内部加全局 try/except,并发送event: error
  3. 检查 Nginx / Cloudflare:如果你的 FastAPI 前面有反向代理,它们默认会缓存或超时短连接。Nginx 需要加:
    location /api/dax/stream { proxy_pass http://fastapi_backend; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; proxy_cache_bypass $http_upgrade; # 关键:禁用代理超时 proxy_read_timeout 300; proxy_send_timeout 300; }

5.2 Tool Call 不触发:LLM “假装”能干,实际不调用

现象:LLM 一直在输出reasoning,比如“我需要查询 Sales 表...”,但 never 发出tool_use事件,最后超时返回。

原因与解法

  • 原因1:Tool 描述太弱。你的@Tool()函数的 docstring 如果是"Execute a DAX query",LLM 认为这太泛,不如自己编。改成"Execute the exact DAX statement provided. This is the ONLY way to get real data from the database. Do NOT make up results.",并加上参数描述"dax_statement: A syntactically correct DAX EVALUATE statement"。我加了这句话后,tool call 触发率从 45% 提升到 89%。
  • 原因2:输入信息不足DAXQueryRequest里的available_tables是空列表,LLM 不知道有什么表可用,不敢贸然调用。在stream_dax_response开头加校验:
    if not request.available_tables: raise ValueError("available_tables cannot be empty. Please provide at least one table name.")
  • 原因3:模型太“懒”claude-3-haiku这类轻量模型,tool calling 能力远弱于sonnet。实测haiku的 tool call 成功率只有 23%,而sonnet是 89%。别为了省 token 而牺牲核心功能。

5.3 类型解析失败:ValidationError频发

现象stream()pydantic_core._pydantic_core.ValidationError,提示Field requiredInput should be a valid string

根因:LLM 生成的 JSON 不符合你的DAXQueryResponse模型。比如它生成了{"dax_statement": "...", "expected_columns": null},而你的模型里expected_columnsList[str],不允许null

终极解法永远不要信任 LLM 的输出。在stream_dax_response里,对DAXQueryResponse做兜底:

elif isinstance(chunk, DAXQueryResponse): # 兜底:如果 expected_columns 是 None 或空,用 dax_statement 里猜 if not chunk.expected_columns: chunk.expected_columns = guess_columns_from_dax(chunk.dax_statement) yield { "event": "final_response", "data": chunk.model_dump() }

guess_columns_from_dax()是一个简单的正则解析函数,哪怕猜错,也比让整个流失败强。生产环境,宁可给用户一个“可能不准确”的列名,也不要让它卡在 validation error 里。

5.4 生产环境监控:你需要盯住哪三个指标?

一个健康的 streaming agent,必须监控三个黄金指标:

  1. TTFB (Time To First Byte):应该 < 1.5s。超过 2s,用户会觉得“卡”,要查httpx版本、网络延迟、system_prompt复杂度。
  2. Tool Call Success Rate:目标 > 85%。低于 80%,说明 tool 描述或输入数据有问题,要 review@Tooldocstring 和DAXQueryRequest的字段完整性。
  3. Final Response Rate:即DAXQueryResponse成功 yield 的比例。目标 > 95%。如果只有 70%,说明 LLM 经常“说一半就跑”,要调低temperature或增加max_tokens

我用 Prometheus + Grafana 搭了一套简易监控。在stream_dax_response里埋点:

from prometheus_client import Counter, Histogram STREAM_TTFB = Histogram('dax_stream_ttfb_seconds', 'Time to first byte for DAX stream') STREAM_FINAL_RATE = Counter('dax_stream_final_response_total', 'Total final responses', ['status']) async def stream_dax_response(...): start_time = time.time() try: first_chunk = True async for chunk in dax_agent.stream(input_dict): if first_chunk: STREAM_TTFB.observe(time.time() - start_time) first_chunk = False ... if isinstance(chunk, DAXQueryResponse): STREAM_FINAL_RATE.labels(status='success').inc() except Exception as e: STREAM_FINAL_RATE.labels(status='error').inc() raise

这三个指标,就是你每天早上打开 Grafana 第一眼要看的东西。它们比任何日志都更能告诉你,你的 agent 是不是真的 ready for production。

6. 实战经验总结与可扩展方向

我在一个真实的 BI 助手项目里,用这套 Pydantic AI 流式方案替换了原来的 LangChain 方案。上线两周后,数据很说明问题:平均响应时间从 6.2 秒降到 4.1 秒,tool call 错误率从 34% 降到 8%,前端工程师反馈“终于不用在 JS 里写一堆 switch-case 来解析不同 event.type 了”。但最让我欣慰的,不是这些数字,而是团队协作的变化。新来的实习生,第一天就能看懂DAXQueryRequest模型,第二天就能加一个新 tool(比如get_customer_segment),第三天就能写单元测试——因为测试太简单了:mock_agent.stream({"user_question": "..."}),然后 assert yield 的strToolResult是否符合预期。

这套模式的可扩展性,体现在三个层面:

  • 横向扩展(更多工具):加一个send_email_reporttool,只需写一个@Tool()函数,注册到Agent,LLM 自动学会在合适的时候调用它。不需要改任何流式逻辑。
  • 纵向扩展(更复杂流程):要做 multi-step agent?比如“先查销售额,再查利润率,最后对比分析”?Pydantic AI 的Agent.run()支持steps参数,你可以定义一个MultiStepDAXPlan模型,让 LLM 先输出一个步骤列表,再循环调用stream()执行每一步。流式依然保持,只是嵌套了一层。
  • 生态扩展(换模型/换平台):明天你想把 Anthropic 换成 OpenAI?只需改Agent初始化时的clientmodel参数,stream_dax_response函数一行都不用动。因为 Pydantic AI 把模型差异,封装在了client的抽象里。

最后分享一个小技巧:在DAXQueryRequest模型里,加一个debug_mode: bool = Field(default=False)字段。当debug_mode=True时,stream_dax_response函数会 yield 额外的{"event": "debug", "data": {"prompt": full_prompt, "raw_llm_output": raw_text}}。这让你在用户报告“结果不对”时,能立刻拿到完整的上下文,而不是靠猜。这个字段不暴露给前端,只在内部测试时用,却是 debug 效率提升 5 倍的神器。

这个方案没有魔法,它只是把“类型安全”、“异步原生”、“关注点分离”这些老生常谈的工程原则,用 Pydantic AI 这个新工具,扎扎实实地落到了 LLM streaming 的每一行代码里。当你不再为“为什么 tool 没调用”、“为什么流断了”、“为什么返回的 JSON 解析失败”而深夜 debug 时,你就知道,这条路走对了。

http://www.jsqmd.com/news/1178210/

相关文章:

  • AI 芯片全景对比:NVIDIA / AMD / 华为,谁在为你的模型“发电“?
  • 《月华剑士》限定品抢购实战:从信息监控到自动化脚本全攻略
  • 20个CC0素材网站2024年可用性实测:Pexels、Coverr等5个无需梯子访问
  • Java基础实操手记:27个可运行示例+错误对照学习
  • Vivado 2018.3 安装避坑指南:3个常见报错分析与Windows/Linux双系统配置
  • 多维聚合实战:从pandas groupby到银行级生产分析流水线
  • STM32F427ZI与AD5593R的嵌入式信号处理方案
  • C语言模拟STL容器:从内存管理到红黑树实现
  • 2026最新无锡本地漏水检测公司本地精选权威推荐:正规防水补漏公司优选口碑TOP5:卫生间/厨房/阳台/飘窗/地下室渗漏水维修师傅上门 - 即刻修防水
  • 模电面试问题汇总三
  • Agentic o3框架实战:解耦决策与执行的AI智能体生产落地指南
  • 大模型越狱(Jailbreaking)原理与三层实战攻防解析
  • 2026 年我筛选出的 10 个程序员兼职渠道,在校生避雷经验
  • G6D-ASI固态继电器与PIC18F4585的直流负载精准控制方案
  • 2026最新猫眼协议抢票-破军网络
  • 卡地亚中国官方售后服务中心|服务电话及完整官方地址权威信息声明(2026年7月最新) - 卡地亚官方售后中心
  • 高项备考用思维导图:视觉锚点记忆法
  • 多商户收款方案对比:支付宝小程序3种支付架构与选型指南
  • Typora 1.x 编辑器宽度自定义:3种CSS修改方案与百分比/像素值对比
  • Java里的RPC:远程方法调用技术,一文带你搞懂
  • 前端音频播放性能优化:Vue 3 组件封装与预加载策略实测
  • 零代码AI开发Chrome插件:从创意到月入5000美元的完整指南
  • YAML配置最佳实践:解耦环境、实验与业务配置
  • 双节锂电池主动均衡系统设计与MP2672A应用
  • Pandas数据整形+Plotly交互可视化实战指南
  • EM3080-W与PIC18F57Q43在条码识别系统中的硬件协同设计
  • 数据大屏素材自动整理:OpenClaw 批量处理图表、适配大屏尺寸、生成素材包
  • 51单片机学习2
  • 上线不是终点:游戏电竞护航陪玩源码系统小程序构建护航俱乐部接单平台运维链路
  • 大模型RL训练新突破:仅训练单层Transformer实现高效优化