生产级智能体框架 Pydantic AI 介绍和使用

一、PydanticAI 介绍

在本专栏前两篇文章中,我们围绕 分布式多智能体 这一核心命题展开了完整的工程实践。

第一篇基于 AgentScope + A2A + Nacos,解决了异构 Agent 之间的通信与协作难题。A2AAgent-to-Agent)协议统一了不同框架下智能体的消息格式与交互规范,Nacos 担任注册中心角色,实现了 Agent 服务的注册、发现与负载均衡,最终达到 Python 端多个 Agent 实例组成集群、客户端无感切换。文章地址如下:

基于A2A + AgentScope + Nacos 构建分布式高可用异构 Agent 方案

第二篇则将视角延伸到了 Java 生态,基于 Spring AI Alibaba 快速搭建 A2A 服务端与客户端,实现了 Java 智能体注册至 Nacos 并被 Python 客户端远程调用,打通了 JavaPython 两套技术栈之间的协作壁垒,文章地址如下:

Spring AI Alibaba + Nacos 构建 A2A 分布式智能体

然而,前两篇文章的重心在于 智能体之间如何协同 ,对于 单个 Python 智能体本身的构建 这个问题上,如何以一种更贴近 Python 开发习惯、更灵活、更生产就绪的方式来实现,还有很多的探索空间。

回到单个 Agent 的构建本身,工程上真正棘手的问题往往不是如何调用模型,而是模型的输出是不是可控,参数类型是否安全、运行时的上下文依赖能否统一管理等等,而 PydanticAI 的出现正是从这个角度切入。

PydanticAI 是由 Pydantic 团队(FastAPIPydantic 背后的团队)推出的新一代 Agent 开发框架,它的核心目标是:以类型安全、结构化、可观测的方式构建生产级 AI Agent

与其他框架不同,PydanticAI 充分利用了 Pydantic 在数据验证上的优势,将模型输入输出全程纳入类型体系管理,同时提供了完备的流式输出、工具调用、依赖注入、Capability 扩展等机制,让 Agent 开发更工程化、更易于测试和维护。

核心特性:

  • 类型安全 :基于 Pydantic 的数据验证,输入输出全程类型检查。
  • 结构化输出 :原生支持将模型输出解析为 Pydantic 模型或 Union 类型。
  • 多模型支持OpenAIAnthropicGeminiOllama 等主流模型开箱即用。
  • 依赖注入 :内置优雅的依赖注入机制,通过 deps_typeRunContext 实现运行时依赖传递。
  • 流式输出 :提供 run_streamiterstream_events 等多种流式接口。
  • Capability 扩展 :通过 AbstractCapability 实现 Agent 全生命周期 Hooks 拦截与扩展。
  • 可观测 :支持 Logfire / OpenTelemetry,工具调用、思考过程全链路可见。

官方介绍文档:https://pydantic.dev/docs/ai/overview/

二、快速上手

2.1 安装

bash 复制代码
pip install pydantic-ai
## 或者
uv add pydantic-ai

根据使用的模型,安装对应扩展:

bash 复制代码
pip install "pydantic-ai-slim[openai]"         # OpenAI / DeepSeek / 兼容接口
pip install "pydantic-ai-slim[anthropic]"      # Claude
pip install "pydantic-ai-slim[vertexai]"       # Google Gemini

2.2 创建 Agent

PydanticAI 的核心入口是 Agent 类:

python 复制代码
from pydantic_ai import Agent

agent = Agent(
    'openai:deepseek-v4-pro',
    instructions='你叫小毕超,是一个超级人工智能助手。',
)

if __name__ == '__main__':
    ## 同步调用
    result = agent.run_sync('你是谁?能做什么?')
    print(result.output)

模型字符串格式{provider}:{model_name}, 例如:

  • openai:gpt-4o:调用 OpenAIgpt-4o
  • openai:deepseek-v4-pro:通过 OpenAI 兼容接口调用 DeepSeek
  • anthropic:claude-sonnet-4-5:调用 AnthropicClaude

OpenAI 格式下如果需要使用 Chat Completions API格式,可以使用前缀为:openai-chat,如 openai-chat:deepseek-v4-pro

模型连接信息,PydanticAI 会自动读取环境变量中的值,因此可以通过环境变量配置模型连接信息,例如使用 ModelScope 上的在线免费API:

bash 复制代码
export OPENAI_BASE_URL=https://api-inference.modelscope.cn/v1
export OPENAI_API_KEY=sk-xxx ## 你的 ModelScope 上的 api key

执行体运行效果:

2.3 注册工具

PydanticAI 提供了 @agent.tool 装饰器可以快速为 Agent 注册工具,同时为了保证上下文的联通性,工具函数的第一个参数必须是 RunContext,在工具内可以通过 RunContext 获取到更多依赖以及上下文信息。

例如:

python 复制代码
from pydantic_ai import Agent, RunContext

agent = Agent(
    'openai:deepseek-v4-pro',
    instructions='你叫小毕超,是一个天气助手。',
)


@agent.tool
async def weather_forecast(ctx: RunContext, location: str) -> str:
    """获取指定地区的天气预报"""
    return f'{location} 地区的天气为 晴,25°C,微风。'


result = agent.run_sync('南京地区的天气如何?')
print(result.output)

运行效果:

当然 RunContext 也不是必须的,如果不需要 RunContext ,则可以使用 @agent.tool_plain 定义工具:

python 复制代码
from pydantic_ai import Agent

agent = Agent(
    'openai:deepseek-v4-pro',
    instructions='你叫小毕超,是一个天气助手。',
)


@agent.tool_plain
async def weather_forecast(location: str) -> str:
    """获取指定地区的天气预报"""
    return f'{location} 地区的天气为 晴,25°C,微风。'


result = agent.run_sync('南京地区的天气如何?')
print(result.output)

三、运行模式

3.1 同步与异步

PydanticAI 同时支持同步和异步运行:

python 复制代码
## 同步调用
result = agent.run_sync('你好')
print(result.output)

## 异步调用
import asyncio

async def main():
    result = await agent.run('你好')
    print(result.output)

asyncio.run(main())

3.2 流式输出

PydanticAI 提供了丰富的流式事件类型,通过 agent.iter 可以拿到完整的事件体系。

python 复制代码
from pydantic_ai import Agent, RunContext, PartStartEvent, ThinkingPart, PartDeltaEvent, TextPartDelta, \
    ThinkingPartDelta, ToolCallPartDelta, FinalResultEvent, FunctionToolCallEvent, FunctionToolResultEvent, TextPart

agent = Agent(
    'openai:deepseek-v4-pro',
    instructions='你叫小毕超,是一个天气助手。',
)

@agent.tool_plain
async def weather_forecast(location: str) -> str:
    """获取指定地区的天气预报"""
    return f'{location} 地区的天气为 晴,25°C,微风。'


async def stream_shell_output(node, run, output_tools_result=False):
    if Agent.is_model_request_node(node):
        thinking, answer = "", ""
        async with node.stream(run.ctx) as request_stream:
            final_result_found = False
            async for event in request_stream:
                if isinstance(event, PartStartEvent):
                    print("\n------------------PartStartEvent---------------------")
                    print(f'Starting part {event.index}: {event.part!r}')
                    thinking, answer = "", ""
                    if isinstance(event.part, ThinkingPart):
                        thinking_part = event.part.content
                        if not thinking and thinking_part:
                            print("\n------------------Thinking---------------------")
                        thinking += thinking_part
                        print(thinking_part, end="", flush=True)

                    if isinstance(event.part, TextPart):
                        answer_part = event.part.content
                        if not answer and answer_part:
                            print("\n------------------Answer---------------------")
                            print("LLM回答: ", end="", flush=True)
                        answer += answer_part
                        print(answer, end="", flush=True)

                elif isinstance(event, PartDeltaEvent):
                    if isinstance(event.delta, ThinkingPartDelta):
                        delta = event.delta.content_delta
                        if not thinking:
                            print("\n------------------Thinking---------------------")
                        if delta:
                            print(delta, end="", flush=True)
                            thinking += delta
                    elif isinstance(event.delta, TextPartDelta):
                        delta = event.delta.content_delta
                        if not answer:
                            print("\n------------------Answer---------------------")
                            print("LLM回答: ", end="", flush=True)
                        print(delta, end="", flush=True)
                        answer += delta

    elif Agent.is_call_tools_node(node):
        async with node.stream(run.ctx) as handle_stream:
            async for event in handle_stream:
                if isinstance(event, FunctionToolCallEvent):
                    print("\n------------------Tool Call---------------------")
                    print(
                        f'Calls tool={event.part.tool_name!r} with args={event.part.args} (tool_call_id={event.part.tool_call_id!r})'
                    )
                elif isinstance(event, FunctionToolResultEvent):
                    if output_tools_result:
                        print(
                            f'Tool call {event.tool_call_id!r} returned => {event.part.content}'
                        )

    elif Agent.is_end_node(node):
        print("\n")

async def main():
    async with agent.iter('南京地区的天气如何?') as run:
        async for node in run:
            await stream_shell_output(node, run)

if __name__ == '__main__':
    import asyncio
    asyncio.run(main())

执行输出:

详细执行过程如下所示:

shell 复制代码
------------------PartStartEvent---------------------
Starting part 0: ThinkingPart(content='', id='rs_chatcmpl-383769a7-6d4b-90e6-9d58-7daa9cca9036', provider_name='openai')

------------------Thinking---------------------
用户想知道南京地区的天气情况。我需要调用天气工具来获取南京的天气预报。

让我调用 weather_forecast 函数,参数 location 为 "南京"。
------------------PartStartEvent---------------------
Starting part 1: TextPart(content='好的,', id='msg_chatcmpl-383769a7-6d4b-90e6-9d58-7daa9cca9036', provider_name='openai')

------------------Answer---------------------
LLM回答: 好的,让我为您查询南京的天气情况
------------------PartStartEvent---------------------
Starting part 2: ToolCallPart(tool_name='weather_forecast', args='', tool_call_id='call_151bb8a7e70e45da86250256', id='fc_call_151bb8a7e70e45da86250256', provider_name='openai')

------------------Tool Call---------------------
Calls tool='weather_forecast' with args={"location": "南京"} (tool_call_id='call_151bb8a7e70e45da86250256')

------------------PartStartEvent---------------------
Starting part 0: ThinkingPart(content='', id='rs_chatcmpl-5286b7b5-cf1c-9a10-963f-7702fc7da039', provider_name='openai')

------------------Thinking---------------------
天气结果出来了:南京地区晴天,25°C,微风。这是一个不错的天气,让我把这个结果整理一下告诉用户。
------------------PartStartEvent---------------------
Starting part 1: TextPart(content='为您', id='msg_chatcmpl-5286b7b5-cf1c-9a10-963f-7702fc7da039', provider_name='openai')

------------------Answer---------------------
LLM回答: 为您查询到**南京**的天气情况如下:

- ☀️ **天气**:晴
- 🌡️ **温度**:25°C
- 🌬️ **风力**:微风

今天南京天气不错,阳光明媚,温度舒适,非常适合外出活动!如果您需要了解其他地区的天气,随时告诉我哦~

另外流式输出,还可以通过 Capability 中的 wrap_run_event_stream 实现,可以更好的做到和主流程解耦,后续内容会演示实现过程。

四、依赖注入

4.1 deps_type

Agent 工具需要访问外部服务(数据库、HTTP 客户端、用户上下文等),可以通过 deps_type 注入运行时依赖:

python 复制代码
from dataclasses import dataclass
from datetime import date, datetime
from pydantic_ai import Agent, RunContext


@dataclass
class WeatherService:
    async def get_forecast(self, location: str, forecast_date: date) -> str:
        return f'{location} 地区 {forecast_date} 日 24°C 晴。'

    async def get_historic_weather(self, location: str, forecast_date: date) -> str:
        return f'{location} 地区 {forecast_date} 日 18°C 多云。'


agent = Agent[WeatherService, str](
    'openai:deepseek-v4-pro',
    deps_type=WeatherService,
    output_type=str
)

@agent.system_prompt()
async def build_system(ctx: RunContext[WeatherService]):
    now_data = str(datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
    return f"""
    你叫小毕超,是一个天气助手。
    当前时间:{now_data} 。
    """

@agent.tool
async def weather_forecast(
    ctx: RunContext[WeatherService],
    location: str,
    forecast_date: date,
) -> str:
    if forecast_date >= date.today():
        return await ctx.deps.get_forecast(location, forecast_date)
    else:
        return await ctx.deps.get_historic_weather(location, forecast_date)


async def main():
    result = await agent.run('南京今天的天气如何?', deps=WeatherService())
    print(result.output)
    print("--"*50)
    result = await agent.run('南京昨天的天气如何?', deps=WeatherService())
    print(result.output)

if __name__ == '__main__':
    import asyncio
    asyncio.run(main())

通过 RunContext[WeatherService] 泛型声明,工具内部可以通过 ctx.deps 访问注入的依赖对象。

运行输出:

4.2 动态 System Prompt

在上面的示例中已经使用了动态 System Prompt,通过@agent.system_prompt 注解,可以将依赖 ctx.deps中的信息放置到 Prompt 中:

python 复制代码
from datetime import datetime
from dataclasses import dataclass

from pydantic_ai import Agent, RunContext


@dataclass
class Dep:
    user_id: str
    session_id: str

agent = Agent(
    'openai:deepseek-v4-pro',
    deps_type=Dep,
    output_type=str
)

@agent.system_prompt
async def get_system_prompt(ctx: RunContext[Dep]) -> str:
    now_data = str(datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
    return f"""
    你叫小毕超,是一个天气助手。
    当前时间:{now_data} 。
    用户ID: {ctx.deps.user_id}
    """

五、结构化输出

5.1 Pydantic 模型输出

output_type 参数支持传入 Pydantic 模型,Agent 会自动将模型响应解析为结构化对象:

python 复制代码
from dataclasses import dataclass
from pydantic_ai import Agent

@dataclass
class WeatherResult():
    location: str
    temperature: float
    condition: str

agent = Agent(
    'openai:deepseek-v4-pro',
    output_type=WeatherResult,
    instructions='你叫小毕超,是一个天气助手。',
)

@agent.tool_plain
async def weather_forecast(location: str) -> str:
    """获取指定地区的天气预报"""
    return f'{location} 地区的天气为 晴,25°C,微风。'


async def main():
    result = await agent.run('南京今天的天气如何?')
    report = result.output
    print(f'地点: {report.location}')
    print(f'温度: {report.temperature}°C')
    print(f'天气: {report.condition}')

if __name__ == '__main__':
    import asyncio
    asyncio.run(main())

运行结果:

5.2 Union 类型输出(分流决策)

Union 类型输出适用于需要根据用户意图进行分流决策的场景,例如快速回答和深度分析的双轨路由。

注意:每个类型最好有稳定的判别字段(如 decision),帮助模型稳定产出正确格式。

python 复制代码
from dataclasses import dataclass
from pydantic_ai import Agent
from pydantic import Field
from typing import Literal

@dataclass
class DirectAnswer():
    decision: Literal['direct'] = 'direct'
    answer: str = ""
    confidence: float = Field(ge=0.0, le=1.0)

@dataclass
class NeedAnalysis():
    decision: Literal['analysis'] = 'analysis'
    reason: str = ""

RouterDecision = DirectAnswer | NeedAnalysis

agent = Agent(
    'openai:deepseek-v4-pro',
    output_type=RouterDecision,
    instructions='你叫小毕超,是一个天气助手。',
)

@agent.tool_plain
async def weather_forecast(location: str) -> str:
    """获取指定地区的天气预报"""
    return f'{location} 地区的天气为 晴,25°C,微风。'


async def main():
    questions = [
        "南京今天的天气如何?",
        "分析一下小毕超博主的风格"
    ]
    for q in questions:
        result = await agent.run(q)
        decision = result.output
        if isinstance(decision, DirectAnswer):
            print(f'直接回答: {decision.answer}')
        else:
            print(f'需要分析,原因: {decision.reason}')


if __name__ == '__main__':
    import asyncio
    asyncio.run(main())

运行效果:

六、Capability 扩展机制

CapabilityPydanticAI 最强大的扩展机制,允许开发者在 Agent 的完整生命周期中注入自定义逻辑,实现日志记录、监控、缓存、熔断、工具拦截等能力。

6.1 Capability 生命周期 Hooks

通过继承 AbstractCapability 并实现对应的 Hooks 方法,可以在 Agent 运行的各个阶段插入自定义逻辑:

python 复制代码
from dataclasses import dataclass
from pydantic_ai import Agent, RunContext, AgentRunResult, ModelRequestContext, ModelResponse, ToolCallPart, \
    ToolDefinition
from pydantic_ai.capabilities import (
    AbstractCapability, AgentNode, NodeResult,
    WrapNodeRunHandler, WrapModelRequestHandler,
    RawToolArgs, ValidatedToolArgs, WrapToolValidateHandler,
    WrapToolExecuteHandler, WrapRunHandler
)
from typing import Any, AsyncIterable


@dataclass
class LoggerCapability(AbstractCapability[Any]):
    """日志记录 Capability,观察 Agent 运行的完整生命周期"""

    ## Run 级别 Hooks
    async def before_run(self, ctx: RunContext) -> None:
        print(f'Agent 开始运行')

    async def after_run(self, ctx: RunContext, *, result: AgentRunResult) -> AgentRunResult:
        print(f'Agent 运行完成,输出: {result.output}')
        return result

    async def wrap_run(self, ctx: RunContext, *, handler: WrapRunHandler) -> AgentRunResult:
        print('包装整个 Run 执行')
        return await handler()

    ## 节点级别 Hooks
    async def before_node_run(self, ctx: RunContext[Any], *, node: AgentNode[Any]) -> AgentNode[Any]:
        print(f'节点开始: {type(node).__name__}')
        return node

    async def after_node_run(self, ctx: RunContext[Any], *, node: AgentNode[Any], result: NodeResult[Any]) -> NodeResult[Any]:
        print(f'节点结束: {type(node).__name__}')
        return result

    ## 模型请求级别 Hooks
    async def before_model_request(self, ctx: RunContext[Any], request_context: ModelRequestContext) -> ModelRequestContext:
        print('模型请求前')
        return request_context

    async def after_model_request(self, ctx: RunContext, *, request_context: ModelRequestContext, response: ModelResponse) -> ModelResponse:
        print(f'模型响应后: {response.parts}')
        return response

    async def wrap_model_request(self, ctx: RunContext, *, request_context: ModelRequestContext,
                                 handler: WrapModelRequestHandler) -> ModelResponse:
        print('包装模型请求')
        return await handler(request_context)

    ## 工具执行级别 Hooks
    async def before_tool_execute(self, ctx: RunContext, *, call: ToolCallPart, tool_def: ToolDefinition,
                                  args: ValidatedToolArgs) -> ValidatedToolArgs:
        print(f'工具执行前: {call.tool_name}, 参数: {args}')
        return args

    async def after_tool_execute(self, ctx: RunContext, *, call: ToolCallPart, tool_def: ToolDefinition,
                                 args: ValidatedToolArgs, result: Any) -> Any:
        print(f'工具执行后: {call.tool_name}, 结果: {result}')
        return result

    async def prepare_tools(self, ctx: RunContext[Any], tool_defs: list[ToolDefinition]) -> list[ToolDefinition]:
        print(f'准备工具列表: {[t.name for t in tool_defs]}')
        return tool_defs

agent = Agent(
    'openai:deepseek-v4-pro',
    output_type=str,
    instructions='你叫小毕超,是一个天气助手。',
    capabilities=[LoggerCapability()],
)

@agent.tool_plain
async def weather_forecast(location: str) -> str:
    """获取指定地区的天气预报"""
    return f'{location} 地区的天气为 晴,25°C,微风。'


async def main():
    result = await agent.run('南京的天气如何?')
    print(f'最终输出: {result.output}')


if __name__ == '__main__':
    import asyncio
    asyncio.run(main())

运行结果:

完整输出信息如下:

shell 复制代码
包装整个 Run 执行
Agent 开始运行
节点开始: UserPromptNode
节点结束: UserPromptNode
节点开始: ModelRequestNode
准备工具列表: ['weather_forecast']
模型请求前
包装模型请求
模型响应后: [TextPart(content='\n\n好的,让我为您查询南京的天气', id='msg_a7368a5d7faf405ea21dd11a5f33e03b', provider_name='openai'), ToolCallPart(tool_name='weather_forecast', args='{"location": "南京"}', tool_call_id='call_320095b4d5e04f31b752c1df', id='fc_call_320095b4d5e04f31b752c1df', provider_name='openai')]
节点结束: ModelRequestNode
节点开始: CallToolsNode
工具执行前: weather_forecast, 参数: {'location': '南京'}
工具执行后: weather_forecast, 结果: 南京 地区的天气为 晴,25°C,微风。
节点结束: CallToolsNode
节点开始: ModelRequestNode
准备工具列表: ['weather_forecast']
模型请求前
包装模型请求
模型响应后: [TextPart(content='南京今天的天气是**晴天**,气温 **25°C**,伴有**微风**,整体来说是个舒适宜人的好天气!\n\n非常适合出门走走,不过阳光比较充足,建议您外出时注意防晒哦~ 有什么其他需要了解的吗?', id='msg_bf7dc79cbb5e4ed192bb8021b1cbc3df', provider_name='openai')]
节点结束: ModelRequestNode
节点开始: CallToolsNode
节点结束: CallToolsNode
Agent 运行完成,输出: 南京今天的天气是**晴天**,气温 **25°C**,伴有**微风**,整体来说是个舒适宜人的好天气!

非常适合出门走走,不过阳光比较充足,建议您外出时注意防晒哦~ 有什么其他需要了解的吗?
最终输出: 南京今天的天气是**晴天**,气温 **25°C**,伴有**微风**,整体来说是个舒适宜人的好天气!

非常适合出门走走,不过阳光比较充足,建议您外出时注意防晒哦~ 有什么其他需要了解的吗?

Hooks 执行顺序说明

Hooks 触发时机 说明
before_run Agent 开始前 初始化准备工作
wrap_run 包装整个运行 可以完全替换或跳过 Agent 运行
before_node_run 每个节点执行前 可替换或修改节点
after_node_run 每个节点执行后 可修改节点结果
before_model_request 模型请求前 可修改请求上下文
after_model_request 模型响应后 可修改模型响应
wrap_model_request 包装模型请求 可完全替换模型调用
before_tool_execute 工具执行前 可修改工具参数
after_tool_execute 工具执行后 可修改工具结果
prepare_tools 每次模型请求前 可过滤或修改可用工具列表
after_run Agent 完成后 最终结果处理

多个 Capabilitymiddleware 语义串行执行 ,前一个 CapabilityHook 完成后执行下一个。

6.2 工具参数与结果拦截

before_tool_executeafter_tool_execute 可以在工具执行前后拦截并修改参数和结果,实现如参数注入、结果替换等能力:

python 复制代码
async def before_tool_execute(self, ctx, *, call, tool_def, args):
    ## 拦截 location 参数,强制替换为精确地址
    if 'location' in args:
        args['location'] = '南京市雨花台区'
    return args

async def after_tool_execute(self, ctx, *, call, tool_def, args, result):
    ## 拦截工具返回结果,做内容修正
    if '23°C' in result:
        result = result.replace('23°C', '25°C')
    return result

6.3 流式事件 Capability

wrap_run_event_stream 允许在 Capability 层对流式事件进行处理:

python 复制代码
from typing import Any, AsyncIterable
from dataclasses import dataclass
from pydantic_ai import Agent, TextPartDelta, ThinkingPartDelta, \
    PartDeltaEvent, TextPart, ThinkingPart, PartStartEvent, RunContext, AgentStreamEvent, ToolCallEvent, ToolResultEvent
from pydantic_ai.capabilities import AbstractCapability


@dataclass
class StreamCapability(AbstractCapability[Any]):
    '''流式输出Capability'''

    async def wrap_run_event_stream(
            self,
            ctx: RunContext[Any],
            *,
            stream: AsyncIterable[AgentStreamEvent],
    ) -> AsyncIterable[AgentStreamEvent]:
        thinking, answer = "", ""
        async for event in stream:
            if isinstance(event, PartStartEvent):
                if isinstance(event.part, ThinkingPart):
                    thinking_part = event.part.content
                    if not thinking and thinking_part:
                        print("\n------------------Thinking---------------------")
                    thinking += thinking_part
                    print(thinking_part, end="", flush=True)
                if isinstance(event.part, TextPart):
                    answer_part = event.part.content
                    if not answer and answer_part:
                        print("\n------------------Answer---------------------")
                        print("LLM回答: ", end="", flush=True)
                    answer += answer_part
                    print(answer, end="", flush=True)
            elif isinstance(event, PartDeltaEvent):
                if isinstance(event.delta, ThinkingPartDelta):
                    delta = event.delta.content_delta
                    if not thinking:
                        print("\n------------------Thinking---------------------")
                    if delta:
                        print(delta, end="", flush=True)
                        thinking += delta
                elif isinstance(event.delta, TextPartDelta):
                    delta = event.delta.content_delta
                    if not answer:
                        print("\n------------------Answer---------------------")
                        print("LLM回答: ", end="", flush=True)
                    print(delta, end="", flush=True)
                    answer += delta
            elif isinstance(event, ToolCallEvent):
                print("\n------------------Tool Call---------------------")
                print(
                    f'Calls tool={event.part.tool_name!r} with args={event.part.args} (tool_call_id={event.part.tool_call_id!r})'
                )
            elif isinstance(event, ToolResultEvent):
                print(
                    f'Tool call {event.tool_call_id!r} returned => {event.part.content}'
                )
            yield event


agent = Agent(
    'openai:deepseek-v4-pro',
    output_type=str,
    instructions='你叫小毕超,是一个天气助手。',
    capabilities=[StreamCapability()]
)


@agent.tool_plain
async def weather_forecast(location: str) -> str:
    """获取指定地区的天气预报"""
    return f'{location} 地区的天气为 晴,25°C,微风。'


async def main():
    await agent.run('南京的天气如何?')


if __name__ == '__main__':
    import asyncio

    asyncio.run(main())

运行效果:

6.4 Capability 工具渐进式加载

Capability 不仅能拦截,还能为 Agent 动态添加新工具。使用 Capability 类封装工具,可以实现能力的按需加载(defer_loading),下面通过上面介绍的流式输出,观察渐进式加载过程:

python 复制代码
from dataclasses import dataclass
from pydantic_ai import Agent, FunctionToolCallEvent, FunctionToolResultEvent, TextPartDelta, ThinkingPartDelta, \
    PartDeltaEvent, TextPart, ThinkingPart, PartStartEvent, RunContext, AgentStreamEvent, ToolCallEvent, ToolResultEvent
from pydantic_ai.capabilities import Capability, AbstractCapability


@dataclass
class StreamCapability(AbstractCapability[Any]):
    '''流式输出Capability'''

    async def wrap_run_event_stream(
            self,
            ctx: RunContext[Any],
            *,
            stream: AsyncIterable[AgentStreamEvent],
    ) -> AsyncIterable[AgentStreamEvent]:
        thinking, answer = "", ""
        async for event in stream:
            if isinstance(event, PartStartEvent):
                if isinstance(event.part, ThinkingPart):
                    thinking_part = event.part.content
                    if not thinking and thinking_part:
                        print("\n------------------Thinking---------------------")
                    thinking += thinking_part
                    print(thinking_part, end="", flush=True)
                if isinstance(event.part, TextPart):
                    answer_part = event.part.content
                    if not answer and answer_part:
                        print("\n------------------Answer---------------------")
                        print("LLM回答: ", end="", flush=True)
                    answer += answer_part
                    print(answer, end="", flush=True)
            elif isinstance(event, PartDeltaEvent):
                if isinstance(event.delta, ThinkingPartDelta):
                    delta = event.delta.content_delta
                    if not thinking:
                        print("\n------------------Thinking---------------------")
                    if delta:
                        print(delta, end="", flush=True)
                        thinking += delta
                elif isinstance(event.delta, TextPartDelta):
                    delta = event.delta.content_delta
                    if not answer:
                        print("\n------------------Answer---------------------")
                        print("LLM回答: ", end="", flush=True)
                    print(delta, end="", flush=True)
                    answer += delta
            elif isinstance(event, ToolCallEvent):
                print("\n------------------Tool Call---------------------")
                print(
                    f'Calls tool={event.part.tool_name!r} with args={event.part.args} (tool_call_id={event.part.tool_call_id!r})'
                )
            elif isinstance(event, ToolResultEvent):
                print(
                    f'Tool call {event.tool_call_id!r} returned => {event.part.content}'
                )
            yield event

## 定义一个可按需加载的旅行规划能力
travel = Capability(
    id='travel',
    description='获取旅行规划',
    instructions='首先获取该地区的天气,然后根据当日的天气和温度获取旅行规划',
    defer_loading=True,  ## 按需加载:模型主动调用 load_capability 后才激活
)

@travel.tool_plain
def planning(local: str, weather: str) -> str:
    """根据天气规划旅行行程"""
    return f'根据 {local} 的天气 {weather},推荐游览中山陵、玄武湖、鸡鸣寺。'


agent = Agent(
    'openai:deepseek-v4-pro',
    capabilities=[travel, StreamCapability()],
)

@agent.tool_plain
async def weather_forecast(location: str) -> str:
    """获取指定地区的天气预报"""
    return f'{location} 地区的天气为 晴,25°C,微风。'

async def main():
    ## 观察流式内容
    await agent.run('规划一下南京的旅行计划?')


if __name__ == '__main__':
    import asyncio
    asyncio.run(main())

defer_loading=True 表示该 Capability 的工具和指令不会在初始请求中暴露,模型需要主动调用 load_capability 工具才能激活。这有助于减少初始 Token 消耗,并支持更精细的能力管控。

运行效果:

完整的执行日志如下:

shel 复制代码
------------------Thinking---------------------
用户想要规划南京的旅行计划。让我先加载旅行相关的capability,同时也可以搜索一下是否有其他相关工具。
------------------Answer---------------------
LLM回答: 好的!我来帮您规划南京的旅行。让我先加载旅行规划功能,同时查一下南京的天气情况
------------------Tool Call---------------------
Calls tool='load_capability' with args={"id": "travel"} (tool_call_id='call_dc30b48599174ac58b8430ca')

------------------Tool Call---------------------
Calls tool='weather_forecast' with args={"location": "南京"} (tool_call_id='call_a551633a5aa44b28907bf27b')
Tool call 'call_a551633a5aa44b28907bf27b' returned => 南京 地区的天气为 晴,25°C,微风。
Tool call 'call_dc30b48599174ac58b8430ca' returned => {'instructions': '首先获取该地区的天气,然后根据当日的天气和温度获取旅行规划'}

------------------Thinking---------------------
现在我有了天气信息和旅行规划工具。根据加载的旅行能力说明:"首先获取该地区的天气,然后根据当日的天气和温度获取旅行规划"。让我使用planning工具来获取南京的旅行规划。

天气是:晴,25°C,微风。
------------------Answer---------------------
LLM回答: 天气已获取!现在让我根据南京的好天气来生成旅行规划
------------------Tool Call---------------------
Calls tool='planning' with args={"local": "南京", "weather": "晴,25°C,微风"} (tool_call_id='call_6647620f9ad4420e84b2a790')
Tool call 'call_6647620f9ad4420e84b2a790' returned => 根据 南京 的天气 晴,25°C,微风,推荐游览中山陵、玄武湖、鸡鸣寺。

------------------Thinking---------------------
The planning tool has returned a recommendation for Nanjing based on the weather. Let me now present a comprehensive travel plan for Nanjing based on this information.

The recommended attractions are:
- 中山陵 (Sun Yat-sen Mausoleum)
- 玄武湖 (Xuanwu Lake)
- 鸡鸣寺 (Jiming Temple)

Let me create a well-organized travel plan for the user.
------------------Answer---------------------
LLM回答: ## 🏙️ 南京旅行计划

### 🌤️ 今日天气概况
| 项目 | 详情 |
|------|------|
| **天气** | ☀️ 晴 |
| **温度** | 🌡️ 25°C |
| **风力** | 🍃 微风 |

今天是非常适合出游的好天气!温度舒适,微风轻拂,正是游览南京的好时机。

---

### 🗺️ 推荐行程安排

#### 🕗 **上午 --- 中山陵**
- **推荐理由**:晴好天气适合户外登高,中山陵坐落于紫金山南麓,392级台阶拾级而上,视野开阔,气势恢宏。
- **游玩时间**:约 2-3 小时
- **小贴士**:早晨人较少,空气清新,适合拍照。

#### 🕛 **中午 --- 周边午餐**
- 可在中山陵附近品尝南京特色美食:鸭血粉丝汤、盐水鸭、小笼包等。

#### 🕐 **下午 --- 鸡鸣寺**
- **推荐理由**:千年古刹,香火鼎盛,素斋也很有名。25°C 的天气逛寺庙非常舒适。
- **游玩时间**:约 1-1.5 小时
- **小贴士**:寺内樱花季(3-4月)尤为美丽,其他季节也各有韵味。

#### 🕒 **傍晚 --- 玄武湖**
- **推荐理由**:微风轻拂,湖畔漫步绝佳。夕阳下的玄武湖波光粼粼,远眺紫金山和城市天际线,十分惬意。
- **游玩时间**:约 1.5-2 小时
- **小贴士**:可以租一艘小船泛舟湖上,感受"金陵明珠"的魅力。

---

### 📋 行程速览

| 时间 | 地点 | 亮点 |
|------|------|------|
| 上午 8:30-11:30 | 中山陵 | 登高望远,缅怀伟人 |
| 中午 11:30-13:00 | 午餐 | 品尝金陵美食 |
| 下午 13:30-15:00 | 鸡鸣寺 | 千年古刹,静心祈福 |
| 傍晚 15:30-17:30 | 玄武湖 | 湖光山色,悠闲漫步 |

---

### 💡 出行小贴士
- 👟 建议穿舒适的平底鞋,中山陵台阶较多。
- 🧴 晴天注意防晒,带上太阳镜和遮阳帽。
- 🥤 随身携带饮用水,保持水分补充。
- 📸 相机充满电,每个景点都很出片!

祝您南京之旅愉快!🎉

七、快捷启动 Web 服务

PydanticAI 提供了 to_web() 方法,可以将 Agent 一键包装为 Web 服务,快速进行测试和演示:

python 复制代码
from pydantic_ai import Agent

agent = Agent(
    'openai:deepseek-v4-pro',
    output_type=str,
    instructions='你叫小毕超,是一个天气助手。',
)


@agent.tool_plain
async def weather_forecast(location: str) -> str:
    """获取指定地区的天气预报"""
    return f'{location} 地区的天气为 晴,25°C,微风。'

app = agent.to_web()

if __name__ == '__main__':
    import uvicorn
    uvicorn.run(app, host='0.0.0.0', port=8000)

启动后访问 http://localhost:8000 即可进行对话测试。

运行效果:

八、思考强度控制

PydanticAI 提供了 Thinking Capability 原生支持模型的深度思考功能,适用于 DeepSeek-R1ClaudeOpenAI 等支持 Thinking 的模型:

python 复制代码
import asyncio
from pydantic_ai import Agent, RunContext
from pydantic_ai.capabilities import Thinking

agent = Agent(
    'openai:deepseek-r1',
    output_type=str,
    instructions='你叫小毕超,是一个天气助手。',
    capabilities=[Thinking(effort='high')],  ## effort: 'low' | 'medium' | 'high'
)

九、消息历史与多轮对话

PydanticAI 支持通过 message_history 参数传入历史消息,实现多轮对话:

python 复制代码
from pydantic_ai import Agent

agent = Agent(
    'openai:deepseek-v4-pro',
    output_type=str,
    instructions='你叫小毕超,是一个天气助手。',
)

@agent.tool_plain
async def weather_forecast(location: str) -> str:
    """获取指定地区的天气预报"""
    return f'{location} 地区的天气为 晴,25°C,微风。'

async def main():
    message_history = []
    while True:
        question = input("请输入:")
        if not question:
            continue
        result = await agent.run(question, message_history=message_history)
        print(result.output)
        ## 拿到会话内容,交给下一次交互
        message_history = result.all_messages()

if __name__ == '__main__':
    import asyncio
    asyncio.run(main())

也可以直接使用 result.new_messages_json() 将本次运行产生的消息序列化后存入数据库,下次通过 ModelMessagesTypeAdapter.validate_json 还原:

python 复制代码
from pydantic_ai import ModelMessagesTypeAdapter

## 序列化消息,存入数据库
messages_json = result.new_messages_json()

## 从数据库恢复消息
messages = ModelMessagesTypeAdapter.validate_json(messages_json)

十、模型设置与 Token 限额

10.1 ModelSettings

通过 ModelSettings 可以精细控制模型的推理参数:

python 复制代码
from pydantic_ai import Agent, ModelSettings

agent = Agent(
    'openai:deepseek-v4-pro',
    output_type=str,
    instructions='你叫小毕超,是一个天气助手。',
    model_settings=ModelSettings(
        temperature=0.8,
        max_tokens=1024
    )
)

10.2 UsageLimits

UsageLimits 允许在运行时限制 Token 消耗,超出限制会抛出 UsageLimitExceeded 异常:

python 复制代码
from pydantic_ai import Agent, UsageLimits, UsageLimitExceeded

agent = Agent(
    'openai:deepseek-v4-pro',
    output_type=str,
    instructions='你叫小毕超,是一个人工智能助手。',
)

async def main():
    try:
        result = await agent.run(
            '你认识小毕超吗?',
            usage_limits=UsageLimits(output_tokens_limit=10),
        )
        print(result.output)
    except UsageLimitExceeded as e:
        print(f'Token 超限: {e}')


if __name__ == '__main__':
    import asyncio
    asyncio.run(main())

更多特征请参考官方文档

官方文档:https://pydantic.dev/docs/ai/overview

相关推荐
dong_junshuai1 小时前
每天一个开源项目#99 OpenResearch:2.2K星的本地研究Agent工作台
开源·github·agent
花椒技术1 小时前
把 AI 视频接进直播:提前生成、按次生成、持续生成怎么选?
agent·音视频开发
用户8082598666871 小时前
用 LangGraph 重写自己手写的 Agent:框架替你解决了什么,什么它不管
agent
能不能静下心来看1 小时前
从 RAG 到 Agent:跑通两个 Demo 后,我终于分清了这两个词
agent
ZGi.ai1 小时前
知识库权限变了,怎样避免答错?
agent·权限管理·知识库·工作流·zgi·客服运营
大模型真好玩2 小时前
DeepSeek Harness 入门很简单(四)——DeepSeek Harness接入插件
人工智能·agent·deepseek
掰头战士2 小时前
MCP、Skill、Plugin,都是给agent拓展能力,到底有何区别?
typescript·llm·agent
嘟嘟嘟95273 小时前
AI Agent 的边缘困境
人工智能·架构·agent
HYDtomako3 小时前
Agent checkpoint设计
ai·agent·checkpoint·claude code
梦在远山后4 小时前
从需求到架构:我的企业研发 Agent 整体设计
langchain·agent