Structured Output.py

# 核心概念
# Structured Output = 将 LLM 的自然语言输出转为结构化 Python 对象
# 在 LangChain 1.0 中,使用 with_structured_output() 方法结合 Pydantic 模型,可以确保 LLM 返回符合预定义模式的数据。

# 基本用法

# 定义 Pydantic 模型

from typing import List, Optional

from pydantic import BaseModel, Field, model_serializer


class Person(BaseModel):
    # 变量名是英文 (name),描述可以是中文,帮助模型理解
    name: str = Field(description="人的姓名")

    # 变量名是英文 (age)
    age: int = Field(description="人的年龄")

    # 变量名是英文 (job)
    job: Optional[str] = Field(default=None, description="人的职业")

    # 自定义序列化方法,消除警告
    @model_serializer
    def serialize_model(self):
        return {"name": self.name, "age": self.age, "job": self.job}

    # email: Optional[str] = Field(default=None, description="人的邮箱")


import os
import sys
from dotenv import load_dotenv
from langchain.chat_models import init_chat_model
from langchain.agents import create_agent
from langchain_core.tools import tool
from langchain.agents.middleware import AgentMiddleware
from langgraph.checkpoint.memory import InMemorySaver

sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

from init_model import get_chat_model

chat_model = get_chat_model()

# 使用 with_structured_output()


# 核心组件
# 1. Pydantic BaseModel
# 所有结构化输出的数据模型都必须继承 BaseModel:

# from pydantic import BaseModel

# class MyModel(BaseModel):
#     field1: str
#     field2: int

# 2. Field 描述
# 使用 Field() 添加字段描述,帮助 LLM 理解:
from pydantic import Field


# 高级特性
# 可选字段
class Person(BaseModel):
    name: str
    age: Optional[int] = None
    job: Optional[str] = None  # 可以为 None


def demo1():
    # 创建结构化输出的 LLM
    structured_llm = chat_model.with_structured_output(Person, include_raw=False)
    response = structured_llm.invoke(
        "张三是一名 30 岁的软件工程师。请提取信息。按照要求返回JSON格式数据,字段名必须为英文(name/age/job)"
    )
    # print(response)
    print(f"name={response.name} age={response.age} job={response.job}")


# 工作原理
# 传统方式 vs 结构化输出
# 传统方式(繁琐):

# 1. 提示词要求 JSON
# prompt = "以JSON格式返回:{name, age, occupation}"
# response = chat_model.invoke(prompt)

# # 2. 手动解析
# import json

# data = json.loads(response.content)
# # 3. 手动验证类型
# if not isinstance(data["age"], int):
#     raise ValueError("age must be int")
# # 4. 手动创建对象
# person = Person(**data)


# 结构化输出(简洁):
# 一步到位
# structured_llm = chat_model.with_structured_output(Person)
# person = structured_llm.invoke("张三是一名 30 岁的软件工程师")
# ✅ 自动解析、验证、创建对象

# 幕后流程
# 1. Pydantic 模型 → JSON Schema
#    Person → {
#      "type": "object",
#      "properties": {
#        "name": {"type": "string", "description": "姓名"},
#        "age": {"type": "integer", "description": "年龄"}
#      }
#    }
# 2. JSON Schema → LLM (函数调用)
#    LLM 被强制返回符合 schema 的 JSON
# 3. JSON → Pydantic 对象
#    自动验证类型并创建 Person 实例


# 创建结构化输出的 LLM


# 实际应用
# 1. 客户信息提取
class CustomerInfo(BaseModel):
    name: str = Field(description="客户姓名")
    phone: str = Field(description="电话号码")
    email: Optional[str] = Field(None, description="邮箱")
    issue: str = Field(description="问题描述")


def demo2():
    structured_llm = chat_model.with_structured_output(CustomerInfo)
    conversation = "客户: 我是李明,电话 138-1234-5678,问题是:订单没发货.仅返回JSON数据,不添加任何额外说明文字"
    info = structured_llm.invoke(f"提取客户信息:{conversation}")
    print(info)


#       return self.__pydantic_serializer__.to_python(
# name='李明' phone='138-1234-5678' email=None issue='订单没发货'


# 应用:
# 自动填充 CRM 系统
# 工单自动分类
# 客服辅助


# 2. 产品评论分析
class Review(BaseModel):
    product: str
    rating: int = Field(description="评分 1-5")
    pros: List[str] = Field(description="优点列表")
    cons: List[str] = Field(description="缺点列表")


def demo3():
    structured_llm = chat_model.with_structured_output(Review)
    review = structured_llm.invoke(
        """
       iPhone 15 很棒!摄像头强大,手感好。但是价格贵,没有充电器。4分。仅返回JSON数据,不添加任何额外说明文字
    """
    )
    print(review)


# return self.__pydantic_serializer__.to_python(
# product='iPhone 15' rating=4 pros=['摄像头强大', '手感好'] cons=['价格贵', '没有充电器']
# 应用:

# 批量处理用户评论
# 自动生成分析报告
# 发现产品改进点


# 3. 文档信息提取 嵌套模型
# 1. 定义子模型:商品明细
class InvoiceItem(BaseModel):
    name: str = Field(description="商品名称")
    amount: float = Field(description="商品单价或该项小计金额")
    # quantity: int = Field(default=1, description="数量,如果未提及默认为1") # 可选


# 2. 定义主模型:发票
class Invoice(BaseModel):
    invoice_number: str = Field(description="发票号")
    date: str = Field(description="日期,格式务必为 YYYY-MM-DD")
    total_amount: float = Field(description="发票总金额")
    items: List[InvoiceItem] = Field(description="商品明细列表,包含名称和价格")


def demo4():
    structured_llm = chat_model.with_structured_output(Invoice)
    document = """
    发票号:20230801001
    日期:2023-08-01
    总金额:1234.56
    项目:
    1. 商品A - 100.00
    2. 商品B - 200.00
    3. 服务费 - 934.56
    """
    # 优化后的 Prompt:更自然,不需要强调“仅返回JSON”,因为 structured_output 已经做了约束
    prompt_text = f"""
    请从以下文本中提取发票信息,并**以 JSON 格式返回**结果:
    {document}
    注意:
    1. 确保日期格式为 YYYY-MM-DD。
    2. 将每个项目拆分为名称和对应的金额。
    3. 如果总金额与各项之和不符,以文档中明确写出的"总金额"为准。
    """
    info: Invoice = structured_llm.invoke(prompt_text)
    print(info)


# 应用:

# 自动化财务处理
# OCR 后结构化
# 数据录入

# 最佳实践
# # 1. 使用清晰的字段描述
# class Good(BaseModel):
#     created_at: str = Field(description="创建时间,格式 YYYY-MM-DD")

# class Bad(BaseModel):
#     created_at: str  # 没有描述,LLM 可能格式错误

# # 2. 合理使用 Optional
# class Good(BaseModel):
#     email: Optional[str] = None  # 邮箱可能没有

# class Bad(BaseModel):
#     email: str  # 强制必填,可能导致提取失败

# # 3. 使用枚举限制值
# class Good(BaseModel):
#     status: Status  # 枚举

# class Bad(BaseModel):
#     status: str  # 可能返回任意字符串

# # 4. 列表设置合理的描述
# class Good(BaseModel):
#     tags: List[str] = Field(description="产品标签,如 '电子产品', '手机'")

# class Bad(BaseModel):
#     tags: List[str]  # LLM 不知道该提取什么

# # 5. 嵌套模型保持简单
# class Good(BaseModel):
#     user: User      # 1 层嵌套
#     settings: dict  # 复杂数据用 dict

# class Bad(BaseModel):
#     user: User
#         company: Company
#             address: Address
#                 country: Country  # 4 层嵌套,容易出错


if __name__ == "__main__":
    # demo1()
    # demo2()
    # demo3()
    demo4()
添加新评论