This commit is contained in:
liubiren 2026-08-13 20:09:34 +08:00
parent 5b5f41be49
commit 92674c434b
1 changed files with 120 additions and 65 deletions

View File

@ -7,7 +7,7 @@ import datetime
from typing import AsyncGenerator
from logfire_api.variables import ValueDoesNotEqual
from pydantic import BaseModel, Field, field_validator
from pydantic import BaseModel, Field, field_validator, TypeAdapter
from pydantic_ai import (
Agent,
ApprovalRequired,
@ -23,6 +23,7 @@ from pydantic_ai import (
from pydantic_ai.models.openai import OpenAIChatModel
from pydantic_ai.providers.openai import OpenAIProvider
from sqlalchemy.sql.dml import ReturningDelete
from pydantic_ai.run import AgentRunResultEvent
DEEPSEEK_V4_FLASH_MODEL = OpenAIChatModel(
@ -38,17 +39,6 @@ MODEL_SETTINGS = ModelSettings(
) # 禁用思考模式
class Deps(BaseModel):
"""
依赖类
"""
date: datetime.date = Field(..., description="日期")
origin_airport_code: str = Field(..., description="出发机场代码")
destination_airport_code: str = Field(..., description="到达机场代码")
source_material: str = Field(..., description="来源资料")
class Flight(BaseModel):
"""
航班类
@ -79,18 +69,35 @@ class FlightNoFound(BaseModel):
"""
system_prompt = """你的任务是根据用户提供的日期、出发机场和到达机场选择航班。
1. 使用 extract_flights 从来源资料提取所有航班信息
2. 使用 select_flight 从所有航班信息中选择匹配航班
"""
class Deps(BaseModel):
"""
依赖类
"""
# 主智能体
agent = Agent[Deps, Flight | FlightNoFound](
date: datetime.date = Field(..., description="日期")
origin_airport_code: str = Field(..., description="出发机场代码")
destination_airport_code: str = Field(..., description="到达机场代码")
source_material: str = Field(..., description="来源资料")
extracted_flights: list[Flight] | None = Field(
default=None, description="提取到的所有航班信息"
)
selected_flight: Flight | FlightNoFound | None = Field(
default=None, description="查询到的航班"
)
# 航班查询与预定智能体
agent = Agent[Deps, Flight | FlightNoFound | DeferredToolRequests](
model=DEEPSEEK_V4_FLASH_MODEL,
model_settings=MODEL_SETTINGS,
deps_type=Deps,
output_type=Flight | FlightNoFound,
system_prompt=system_prompt,
output_type=Flight | FlightNoFound | DeferredToolRequests,
system_prompt=(
"你的任务是帮助用户查询并预定航班,**必须按照下述步骤执行**",
"1. 使用 extract_flights 提取所有航班信息;",
"2. 使用 select_flight 查询航班;",
"3. 若查询到航班则使用 book_flight 预定航班;若未查询到航班则返回 FlightNoFound。",
),
)
# 航班信息提取智能体
@ -99,64 +106,78 @@ extraction_agent = Agent(
model_settings=MODEL_SETTINGS,
output_type=list[Flight],
system_prompt=(
"你的任务是根据给定的来源资料提取出所有航班信息,包括航班号、日期、出发机场代码、到达机场代码和机票价格。",
"你的任务是根据提供的来源资料提取出所有航班信息,包括航班号、日期、出发机场代码、到达机场代码和机票价格。",
"其中出发机场代码和到达机场代码须为英文、大写、3位字符串例如 San Francisco International Airport (SFO) 的机场代码为 SFO。",
"若某航班信息不全则跳过该航班,若没有任何航班信息则返回空列表。",
"禁止编造。",
),
) # 通过提示词约束输出
# 航班选择智能体
selection_agent = Agent(
model=DEEPSEEK_V4_FLASH_MODEL,
model_settings=MODEL_SETTINGS,
output_type=Flight | FlightNoFound,
system_prompt="你的任务是从所有航班信息中选择匹配用户提供的日期、出发机场和到达机场的航班:若有多个航班则选择最便宜的航班,选择完成需用户确认是否预定;若没有则返回 FlightNoFound。",
)
"""
如何提高提取准确率
1. 系统提示词规则约束
2. 降低模型温度
3. 输出类型约定和输出模型校验
4. 业务规则约束例如输出不可为空列表
"""
@agent.tool
async def extract_flights(ctx: RunContext[Deps]) -> list[Flight]:
"""
提取所有航班信息
**如何提高提取准确率**
1. 系统提示词规则约束
2. 降低模型温度
3. 输出类型约定和输出模型校验
4. 业务规则约束例如输出不可为空列表
"""
result = await extraction_agent.run(
ctx.deps.source_material,
usage_limits=UsageLimits(request_limit=3),
usage=ctx.usage,
usage_limits=UsageLimits(request_limit=10),
)
if len(output := result.output) != 8:
raise ModelRetry("提取到的所有航班信息应为 8 条")
if (flight_counts := len(output := result.output)) != 8: # 模拟业务规则约束
raise ModelRetry(f"所有航班信息数量应为 8 ,不是 {flight_counts}")
# 更新提取到的航班信息
ctx.deps.extracted_flights = output
return output
# 航班查询智能体
selection_agent = Agent[Deps, Flight | FlightNoFound](
model=DEEPSEEK_V4_FLASH_MODEL,
model_settings=MODEL_SETTINGS,
deps_type=Deps,
output_type=Flight | FlightNoFound,
system_prompt=(
"你的任务是在提取到的所有航班信息中查询匹配用户提供的日期、出发机场代码和到达机场代码的机票价格最低的航班。",
"若没有符合要求的航班则返回 FlightNoFound。",
),
)
@agent.tool
async def select_flight(ctx: RunContext[Deps]) -> Flight | FlightNoFound:
"""
选择航班
查询航班
"""
result = await selection_agent.run(ctx.deps.source_material, usage=ctx.usage)
if isinstance(result.output, Flight):
if not ctx.tool_call_approved:
raise ApprovalRequired(metadata={"reason": "protected"})
return result.output
extracted_flights = ctx.deps.extracted_flights
if not extracted_flights:
raise ModelRetry("须先使用 extract_flights 提取所有航班信息")
result = await selection_agent.run(
(
f"{ctx.prompt}",
f"所有航班信息:\n{TypeAdapter(list[Flight]).dump_json(extracted_flights).decode('utf-8')}",
),
deps=ctx.deps,
usage=ctx.usage,
usage_limits=UsageLimits(request_limit=10),
)
ctx.deps.selected_flight = (output := result.output)
return output
@agent.output_validator
@selection_agent.output_validator
async def validate_output(
ctx: RunContext[Deps], output: Flight | FlightNoFound
) -> Flight | FlightNoFound:
"""
输出校验
航班查询智能体输出校验
"""
# 不校验未查询到航班
if isinstance(output, FlightNoFound):
@ -165,17 +186,39 @@ async def validate_output(
errors = []
if output.date != ctx.deps.date:
errors.append(f"航班日期应为 {ctx.deps.date}, 不是 {output.date}")
if output.origin != ctx.deps.origin:
errors.append(f"航班出发机场应为 {ctx.deps.origin}, 不是 {output.origin}")
if output.destination != ctx.deps.destination:
if output.origin_airport_code != ctx.deps.origin_airport_code:
errors.append(
f"航班到达机场应为 {ctx.deps.destination}, 不是 {output.destination}"
f"航班出发机场应为 {ctx.deps.origin_airport_code}, 不是 {output.origin_airport_code}"
)
if output.destination_airport_code != ctx.deps.destination_airport_code:
errors.append(
f"航班到达机场应为 {ctx.deps.destination_airport_code}, 不是 {output.destination_airport_code}"
)
if errors:
raise ModelRetry("\n".join(errors))
return output
@agent.tool
async def book_flight(ctx: RunContext[Deps]) -> Flight | FlightNoFound:
"""
预定航班
"""
if not (selected_flight := ctx.deps.selected_flight):
raise ModelRetry("须先使用 select_flight 查询航班")
if isinstance(selected_flight, FlightNoFound):
return selected_flight
else:
if not ctx.tool_call_approved:
raise ApprovalRequired(
metadata={
"reason": f"需要用户确认是否预定 {selected_flight.number}{selected_flight.date.strftime('%Y-%m-%d')}{selected_flight.origin_airport_code}{selected_flight.destination_airport_code} 的航班"
}
)
return selected_flight
source_material = """
1. Flight SFO-AK123
- Price: $350
@ -221,8 +264,8 @@ source_material = """
async def run_stream_events(
user_prompt: str,
deps: Deps,
user_prompt: str,
message_history: list[ModelMessage],
usage: RunUsage,
deferred_tool_results: DeferredToolResults | None = None,
@ -239,30 +282,42 @@ async def run_stream_events(
async def main():
# 实例化依赖
deps = Deps(
source_material=source_material,
date=datetime.date(2025, 1, 10),
origin="SFO",
destination="ANC",
origin_airport_code="SFO",
destination_airport_code="ANC",
selected_flight=None,
)
# message_history 传空列表,不要传 None
msg_history: list[ModelMessage] = []
prompt = f"Find me a flight from {deps.origin} to {deps.destination} on {deps.date}"
gen = run_stream_events(
user_prompt=prompt,
user_prompt = f"帮我查询并预定在 {deps.date.strftime('%Y-%m-%d')}{deps.origin_airport_code}{deps.destination_airport_code} 的航班"
message_history: list[ModelMessage] = []
events = run_stream_events(
deps=deps,
message_history=msg_history,
user_prompt=user_prompt,
message_history=message_history,
usage=RunUsage(),
)
# 消费异步生成器
async for ev in gen:
print(ev)
async for event in events:
print(event)
if isinstance(event, AgentRunResultEvent):
# 保存本次运行新增消息
message_history.extend(event.result.new_messages())
asyncio.run(main())
"""
@selection_agent.tool
async def select_flight(ctx: RunContext[Deps]) -> Flight | FlightNoFound:
result = await selection_agent.run(ctx.deps.source_material, usage=ctx.usage)
if isinstance(result.output, Flight):
if not ctx.tool_call_approved:
raise ApprovalRequired(metadata={"reason": "protected"})
return result.output
if not isinstance(event, AgentRunResultEvent):
yield event
else: