另外三种模式与类型校验

另外三种模式与类型校验

2026年9月21日·#编程学习/langchain学习笔记LangChain/AI·5437 字 27 分钟
浏览量加载中...
AI 摘要

TypedDict、JSON Schema、dataclass 三种 Schema 写法,用 fake server 实测四种模式的校验差异,以及 include_raw 取原始 AIMessage 和输出解析器

上一篇讲的是 Pydantic(功能最全、唯一会校验)。这一篇补齐另外三种 Schema 写法,并用一个假服务端实测它们遇到”字段不匹配”时到底会怎样。

模式2:TypedDict(轻量的字典结构)#

TypedDict 是 Python 3.8+ 引入的类型提示工具——带有类型声明的字典结构,适合”快速定义字典结构、又不需要 Pydantic 那套重量级功能”的场景。

普通 dict 没有类型信息,看不出该有哪些字段:

{
"title": "盗梦空间",
"year": 2010,
"director": "克里斯托弗·诺兰",
"rating": 9.3
}

TypedDict 可以进一步说明:这个字典应该有哪些字段、每个字段是什么类型:

from typing_extensions import TypedDict
class MovieDict(TypedDict):
title: str
year: int
director: str
rating: float
movie: MovieDict = {
"title1": "盗梦空间", # ← 故意写错字段名
"year": 2010,
"director": "克里斯托弗·诺兰",
"rating": 8.8,
}
print(movie)
# {'title1': '盗梦空间', 'year': 2010, 'director': '克里斯托弗·诺兰', 'rating': 8.8}

图:字段名故意写成 title1 的 TypedDict 示例(课程实测截图)

Warning

TypedDict 主要是类型声明,不是运行时强校验器。 上面字段名 title 写成了 title1只有 IDE 的静态类型检查会标记,运行起来照样输出——不会抛异常。

Annotated:给类型附加说明#

Annotated 用来在”类型”之外再附加额外信息(即元数据),作用类似 Pydantic 的 Field

Annotated[类型, 附加信息1, 附加信息2, ...]
from typing_extensions import TypedDict, Annotated
class MovieDict(TypedDict):
"""电影的详细信息"""
title: Annotated[str, ..., "电影标题"]
year: Annotated[int, ..., "电影上映年份"]
director: Annotated[str, Ellipsis, "导演"]
rating: Annotated[float, "电影评分,满分十分"]

... 是 Python 的字面量,等价于 Ellipsis可以理解为占位符:下游框架(如 LangChain)能对它做定制化处理——在 LangChain 里 Annotated... 表示”该字段必须存在、不可省略”,用来指示模型的输出。

课程用真实模型验证过一次”必填 vs 非必填”的区别:让模型抽取”盗梦空间上映于2010年,我们并不知道它的导演是谁”。

字段标记输出结果
directorAnnotated[str, ..., "导演"](必填)字典里这个键,值为空字符串 ''
ratingAnnotated[float, "电影评分,满分十分"](未标记必填)字典里直接没有这个键

嵌套结构:把列表塞进字典#

TypedDict 也能定义嵌套——字段的值是”另一个 TypedDict 的列表”,写法是 Annotated[List[Actor], "演员列表"]

from typing import TypedDict, List, Annotated
# 使用 TypedDict 定义嵌套结构
class Actor(TypedDict):
"""演员情况"""
name: Annotated[str, "演员姓名"]
role: Annotated[str, "饰演的角色"]
class Movie(TypedDict):
"""电影情况"""
title: Annotated[str, "电影标题"]
year: Annotated[int, "上映年份"]
director: Annotated[str, "导演"]
cast: Annotated[List[Actor], "演员列表"] # ← 嵌套列表定义
rating: Annotated[float, "评分"]
structured_llm = model.with_structured_output(Movie)
resp = structured_llm.invoke("给我介绍下电影《盗梦空间》")
# 访问嵌套数据:字典就用 ['键'] 一层层取
print(f"电影名: {resp['title']}")
print(f"上映年份: {resp['year']}")
print(f"导演: {resp['director']}")
print(f"演员列表:{resp['cast']}")
print(f"评分: {resp['rating']}")

输出——和 Pydantic 的 .cast[0].name 不同,TypedDict 全程是字典取值

电影名: 盗梦空间
上映年份: 2010
导演: 克里斯托弗·诺兰
演员列表:[{'name': '莱昂纳多·迪卡普里奥', 'role': '多姆·柯布'}, {'name': '约瑟夫·高登-莱维特', 'role': '亚瑟'}, {'name': '艾伦·佩吉', 'role': '阿里阿德涅'}, {'name': '汤姆·哈迪', 'role': '伊姆斯'}, {'name': '渡边谦', 'role': '斋藤'}]
评分: 8.8
Important

取值方式是最容易踩的差异

  • Pydantic:result.cast[0].name点属性
  • TypedDict / JSON Schema / dataclass:resp["cast"][0]["name"]方括号

resp['cast'] 本身就是一个普通列表,里面的元素是普通字典——没有任何对象包装。所以”用过 Pydantic 再换 TypedDict”,第一件事就是把点号全改回方括号。

实测(本机 + 假服务端)resp = {'title': '盗梦空间', ..., 'cast': [{...}, {...}], 'rating': 8.8}type(resp)dictresp['cast'][0]['name'] 取到 '莱昂纳多·迪卡普里奥'——与课程一致。

... 的厂商差异#

前面说”... 表示字段必须存在”,但缺字段时到底填什么,和模型提供商有关系

同一个 MovieDict(四个字段全标 ...),问”根据这段话抽取盗梦空间的信息,不包含的信息可以留空:盗梦空间在 2010 年上映,导演是克里斯托弗·诺兰”(没提评分):

平台输出表现
CloseAIgpt-5.4-mini{'title': '盗梦空间', 'year': 2010, 'director': '克里斯托弗·诺兰'}直接省略了未标记必填的字段
OpenRouteropenai/gpt-5.4-mini{'title': '盗梦空间', 'year': 2010, 'director': '克里斯托弗·诺兰', 'rating': 0}补了一个 rating=0

一个”省略”,一个”补 0”——都是”必填标记”被处理的方式,但结果完全不同。

再看”标记必填 vs 没标记”的对照实验(同一份 Schema,其中 director...rating 不标),问”盗梦空间上映于2010年,我们并不知道它的导演是谁”:

{'title': '盗梦空间', 'year': 2010, 'director': ''}

directorrating 的信息都缺失,但前者被标记为必填,因此输出的字典包含该字段、值为空字符串;而 rating 字段被直接省略了。

Warning

“必填”的语义只保证”键存在”,不保证”值有内容”——必填字段拿不到信息时就给 ''(或 0),照样能把你的下游逻辑骗过去。 加上厂商差异(OpenRouter 会给未标记的字段也补 rating=0,CloseAI 会省略),结论是:判断”字段到底有没有抽到值”,不要只看键在不在

# 不推荐:键在就以为抽到了
if "rating" in resp: ...
# 推荐:连值一起判断(必填字段可能是 '' / 0)
if resp.get("rating"): ...

模式3:JSON Schema(不推荐)#

这种方式需要按 JSON Schema 规范手写字典,比较繁琐,并且缺少校验机制,课程明确不推荐:

project_schema = {
"title": "MovieInfo",
"description": "包含电影标题、上映年份、导演、演员和评分的电影对象",
"type": "object",
"properties": {
"title": {"type": "string", "description": "电影标题"},
"year": {"type": "integer", "description": "上映年份"},
"director": {"type": "string", "description": "导演"},
"cast": { # 嵌套数组要手写到底
"type": "array",
"description": "演员列表",
"items": {
"type": "object",
"properties": {
"name": {"type": "string", "description": "演员姓名"},
"role": {"type": "string", "description": "演员角色"},
},
"required": ["name", "role"],
},
},
"rating": {"type": "number", "description": "评分(10分制)"},
},
"required": ["title", "year", "director", "cast", "rating"],
}
structured_model = model.with_structured_output(project_schema)
response = structured_model.invoke("生成一个关于《星际穿越》的电影信息,包含导演、演员、评分")
print(response) # 返回字典

唯一的优势:与前后端/跨语言接口最通用(JSON Schema 本身是通用标准,不绑定 Python)。代价:嵌套稍微深一点就写得又长又容易出错。

显式指定 method="json_schema"#

上面那次 JSON Schema 的调用,其实是”透传”路线——JSON Schema 也可以走它自己的专用模式,用 method 参数显式指定:

json_schema = {
"title": "Movie",
"description": "A movie with details",
"type": "object",
"properties": {
"title": {"type": "string", "description": "The title of the movie"},
"year": {"type": "integer", "description": "The year the movie was released"},
"director": {"type": "string", "description": "The director of the movie"},
"rating": {"type": "number", "description": "The movie's rating out of 10"},
},
"required": ["title", "year", "director", "rating"],
}
structured_model = model.with_structured_output(
json_schema,
method="json_schema", # ← 显式指定走服务器的 json_schema 模式
)
response = structured_model.invoke("给出盗梦空间的信息")
print(response) # 返回字典:{'title': '盗梦空间', 'year': 2010, 'director': '克里斯托弗·诺兰', 'rating': 8.8}
print(type(response)) # <class 'dict'>

两个必须知道的点:

  1. method 是否可用,依赖于模型供应商及 LangChain 适配器的具体实现。课程明确说明:DeepSeek 模型服务不支持 json_schema 模式——所以这行代码在 DeepSeek 上跑不通,要挑支持它的平台
  2. json_schema 字典里的关键字是固定写法(遵循 JSON Schema 规范),课程给出了逐项解释:
关键字含义
title为整个 Schema 或特定属性提供一个人类可读的标题不能是中文,用于提高可读性
description更详细的文字描述,说明 Schema 或属性的用途,和 title 一样旨在帮助理解
type定义当前数据节点必须是什么数据类型(stringnumberintegerbooleanobjectarraynull
properties定义 JSON 对象里可以包含哪些属性(键),以及每个属性对应的值类型和说明
requiredtype"object" 时使用,数组,列出对象中必须存在的属性名
Note

title 不能是中文——而 title 同时还被当作工具/函数的 nameconvert_to_openai_functionpop("title") 拿来做函数名),所以它得是个合法标识符。想写中文说明就放到 description 里。 另外注意:上面”不传 method”的写法,LangChain 会走**工具调用(Tool Calling)**路线把 Schema 递过去;显式传 method="json_schema" 才是用平台自己的 response_format 式的强约束能力。两种路线在支持性上不是一回事:Tool Calling 大多数模型都支持,json_schema 模式则要看平台脸色。

模式4:@dataclass#

@dataclass 是 Python 标准库 dataclasses 提供的类装饰器,用于简化”以字段为核心”的数据类定义。加上它以后,Python 会根据字段声明自动生成常用方法

  • __init__
  • __repr__
  • __eq__
from dataclasses import dataclass
from pydantic import Field
@dataclass
class Movie:
"""电影的详细信息"""
title: str = Field(description="电影标题")
year: int = Field(description="电影上映年份")
director: str = Field(description="导演")
rating: float = Field(description="电影评分,满分十分")
structured_model = model.with_structured_output(Movie)
response = structured_model.invoke("给出盗梦空间的信息")
print(response) # {'title': '盗梦空间', 'year': 2010, ...}
print(type(response)) # <class 'dict'> ← 未经校验的字典
Important

@dataclass 修饰的类 ≠ 手写 __init__ 的普通类。虽然行为上很接近,但 @dataclass 修饰后会携带字段元信息,所以它能作为 LangChain 的 Schema

课程说”手写 __init__ 的普通类不能当 Schema”,实测要打个补丁:直接传普通类给 with_structured_output不会报错,但生成的 Schema 里每个字段都是空对象——既没有类型也没有描述,模型等于在盲抽:

# convert_to_openai_tool(手写类) 的结果
"parameters": {"properties": {"title": {}, "year": {}}, "required": ["title", "year"], "type": "object"}
# 对比 @dataclass 版本:{"title": {"description": "电影标题", "type": "string"}, ...}

所以结论是:@dataclass 可以用,普通类”能用但等于没用”——字段描述全丢,抽取质量无从保证。

类型校验:用一个假服务端看清楚#

前面反复说”只有 Pydantic 会校验”,怎么亲眼看到这个差异?——自己造一个假的模型服务端。

fake server 的作用:客户端收到的响应是人为构造的,从而能观察不同模式下 LangChain 会从响应里的哪些字段抽取信息、以及会不会校验。

服务端:固定返回一份”字段不匹配”的响应#

import json
import time
from http.server import BaseHTTPRequestHandler, HTTPServer
# 故意把 title 写成 title1,并且不返回 year
FAKE_ARGS = {"title1": "盗梦空间", "director": "克里斯托弗·诺兰", "rating": 9.3}
class FakeDeepSeekHandler(BaseHTTPRequestHandler):
def do_POST(self):
content_length = int(self.headers.get("Content-Length", 0))
raw_body = self.rfile.read(content_length).decode("utf-8")
json_body = json.loads(raw_body)
# 用客户端请求里的工具名回填,模拟"模型按 Schema 返回"
response = {
"id": "chatcmpl-test",
"object": "chat.completion",
"created": int(time.time()),
"model": "any",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "",
"tool_calls": [
{
"id": "call_1",
"type": "function",
"function": {
"name": json_body["tools"][0]["function"]["name"],
"arguments": json.dumps(FAKE_ARGS, ensure_ascii=False),
},
}
],
},
"finish_reason": "tool_calls",
}
],
"usage": {"prompt_tokens": 1, "completion_tokens": 1, "total_tokens": 2},
}
data = json.dumps(response, ensure_ascii=False).encode()
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(data)))
self.end_headers()
self.wfile.write(data)
def log_message(self, *args):
pass
if __name__ == "__main__":
server = HTTPServer(("127.0.0.1", 8889), FakeDeepSeekHandler)
print("fake server 已启动: http://127.0.0.1:8889")
server.serve_forever()

服务端只是把客户端发来的请求打印出来,再用请求里的工具名回填一份固定响应——所以任何兼容 OpenAI 接口的客户端都能接上去。

客户端:把 api_base 指向假服务端#

from langchain_deepseek import ChatDeepSeek
from pydantic import SecretStr
model = ChatDeepSeek(
model="deepseek-v4-flash",
api_base="http://localhost:8889", # ← 指向假服务端
api_key=SecretStr("<KEY>"),
)

课程版响应里的两个细节#

假服务端”造假”的方式,课程选得很有讲究——每一处都对应一个要验证的问题

"arguments": json.dumps(
{'title1': '盗梦空间', 'year2': 2010, 'director': '克里斯托弗·诺兰', 'rating': 9.3},
ensure_ascii=False
)

细节一:固定响应故意错两个字段(title1year2。Schema 里要的是 titleyear

// 假服务端返回的
{"title1": "盗梦空间", "year2": 2010, "director": "克里斯托弗·诺兰", "rating": 9.3}
// LangChain 期望的
{"title": "xxx", "year": xxxx, "director": "xxx", "rating": xxx}

前两个字段不匹配——所以 Pydantic 会报 2 个 Field required 错误,而不是 1 个。错两个字段比错一个更有说服力:既证明”键名对不上会被发现”,也证明”字段缺失会被发现”。

细节二:finish_reason"stop"(而不是 "tool_calls")。这看起来”不对”,但正是这个实验想验证的:客户端解析工具调用时不看 finish_reason——只要 choices[0].message.tool_calls 有内容就照单全收。故意把它写成 "stop",观察结果是否仍然一致。

Note

本机实测(langchain-core 1.2.18 + 同款假服务端):把这两个细节都还原(title1/year2finish_reason: "stop")后:

  • Pydantic:抛 ValidationError: 2 validation errors for MovieModel,两条都是 Field required(正好是 titleyear);
  • TypedDict:原样返回 {'title1': '盗梦空间', 'year2': 2010, 'director': '克里斯托弗·诺兰', 'rating': 9.3}两个错误键名都保留
  • finish_reason: "stop" 完全没有影响解析——结论和课程一致:tool_calls 是否存在,才是”模型要调工具”的唯一判据

顺带一个有用的发现:with_structured_output 发出去的请求里,tool_choice 会被设成”必须调用这个特定函数”

"tool_choice": {"type": "function", "function": {"name": "MovieModel"}}

也就是说结构化输出本质上是”强制调用一个名叫 MovieModel 的假工具,把它返回的参数当成结果”——这正是它能绕开”模型爱写解释性文字”的原因(第 11 篇第 3 步”模型交互与强约束”的落地细节)。

实测结果:四种模式的差异#

用同一份”字段不匹配”的响应,四种模式跑下来(本机用 langchain_deepseek + fake server 复现过):

模式结果返回类型
Pydantic❌ 抛 ValidationErrortitleyear 两个字段 Field required——
TypedDict✅ 原样返回,错误键名 title1 都保留{'title1': '盗梦空间', 'director': '...', 'rating': 9.3}dict
JSON Schema✅ 原样返回(同上)dict
@dataclass✅ 原样返回(同上)dict
小结

用 Pydantic 定义 Schema,接收到响应后会进行校验,字段不匹配则抛出异常;其余三种方式不校验。

顺带一个实测细节:Pydantic 校验失败时不会自动重试(假服务端只收到 1 次请求),异常直接抛给你的代码——要不要重试、怎么重试由你自己决定。

附一:include_raw=True 拿到原始响应#

with_structured_output 可以传 include_raw=True,表示同时返回解析前的原始 AIMessage,从而访问令牌用量等元数据:

model_with_structure = model.with_structured_output(Movie, include_raw=True)
resp = model_with_structure.invoke("给我介绍下电影《星际穿越》")
print(type(resp)) # <class 'dict'>
print(list(resp.keys())) # ['raw', 'parsing_error', 'parsed']
print(resp["parsed"]) # 解析后的 Movie 对象
print(resp["raw"].usage_metadata) # {'input_tokens': 373, 'output_tokens': 93, 'total_tokens': 466, ...}

返回的是三个字段

含义
raw原始 AIMessage(含 token 用量等元数据)
parsed解析后的输出(Pydantic 对象)
parsing_error解析/校验错误(正常时为 None
Note

实测补充:加上 include_raw=True 后,Pydantic 的校验失败不再抛异常,而是变成”正常返回 + parsed=None + parsing_error 里带错误信息”。所以想”失败时降级处理”而不是让程序崩掉,可以走这条路。

附二:用输出解析器获取结构化结果(不推荐)#

除了 with_structured_output,还有一种更传统的路线——输出解析器(Output Parser)。它的思路是:提示词指导 → 模型生成文本 → 解析器转换,即靠提示词明确要求模型输出特定格式的文本,再用解析器把文本转成对象。

from langchain_core.output_parsers import JsonOutputParser
from langchain_core.prompts import ChatPromptTemplate
from pydantic import BaseModel, Field
# 1. 提示词模板(必须自己叮嘱模型输出 JSON)
prompt_template = ChatPromptTemplate.from_messages([
("system", "回答用户问题,必须始终输出一个包含title(电影标题)和year(上映年份)的JSON 对象"),
("human", "问题:{question}"),
])
# 2. 定义结构
class Movie(BaseModel):
"""电影信息"""
title: str = Field(description="电影标题")
year: int = Field(description="上映年份")
# 3. 创建输出解析器
parser = JsonOutputParser(pydantic_object=Movie)
# 4. 用管道符把三段串成链
chain = prompt_template | model | parser
# 5. 调用(返回字典)
result = chain.invoke({"question": "介绍下《盗梦空间》"})
print(result)

不推荐的原因

对比项with_structured_output输出解析器
是否需要”求”模型输出 JSON不需要,靠底层函数调用强约束需要,写在提示词里
稳定性高(模型底层保证格式)依赖模型自觉,容易带解释性文字导致解析失败
代码量一行绑定提示词 + 解析器 + 管道

相关#

练习题#

一、回忆填空(写完再展开对答案)#

  1. TypedDict 是 Python 3.8+ 的____工具,主要作用是类型声明,不是运行时强____
  2. 字段名写错时,TypedDict 只会被____的静态检查标记,运行时不会____
  3. Annotated 用来在类型之外附加____,作用类似 Pydantic 的 ____
  4. ...(即 Ellipsis)在 LangChain 里表示该字段____;课程实测中必填字段缺失时输出空字符串,未标记必填的字段直接____
  5. JSON Schema 模式需要手写字典,缺点是____且____,但它是最通用的——因为 JSON Schema 是____标准
  6. @dataclass 会自动生成 __init____repr______ 三个方法;它能直接当 Schema,而手写 __init__ 的普通类虽然不会报错,但生成的 Schema 字段没有____和____信息
  7. fake server 的作用是:把客户端收到的响应____,用来观察不同模式会从响应里____
  8. 类型校验结论:只有____会校验,字段不匹配抛____;其余三种都原样返回____
  9. include_raw=True 返回三个键:rawparsed、____;其中 token 用量在 raw.____
填空答案(做完再点开)
  1. 类型提示 / 校验器 2. IDE / 报错 3. 额外信息(元数据) / Field 4. 必须存在(不可省略) / 被省略 5. 繁琐 / 缺少校验机制 / 通用(跨语言) 6. __eq__ / 类型 / 描述(字段描述全丢) 7. 人为构造 / 抽取哪些字段(以及是否校验) 8. Pydantic / 异常(ValidationError) / 字典 9. parsing_error / usage_metadata

二、裸写题#

  • 2-1 TypedDict 版 SchemaTypedDict + Annotated 定义一个电影结构(title/year/director 标成必填,rating 不标),绑定 with_structured_output 调用一次,打印结果和它的类型(观察是不是 dict)。

    提示(先自己想,实在想不出再点开)

    一级 · 思路:TypedDict 走的是 dict 路线,不是对象路线 二级 · 方法from typing_extensions import TypedDict, Annotated 三级 · 骨架:描述信息写成 Annotated[str, ..., "电影标题"],第三个参数就是给模型看的说明

  • 2-2 同一份数据用 dataclass 定义 把 2-1 的结构改写成 @dataclass + Pydantic 的 Field(description=...),调用后对比两者返回类型有什么共同点。

    提示

    一级 · 思路:dataclass 是最”Python 原生”的写法,但描述信息要靠 Field 借过来 二级 · 方法from dataclasses import dataclass + from pydantic import Field 三级 · 骨架:字段写成 title: str = Field(description="电影标题"),注意都要有默认值(这里的值就是 Field(...)

  • 2-3 include_raw 看 token 用量include_raw=True 调用一次 Pydantic 结构化输出,打印 resp.keys()resp["parsed"]resp["raw"].usage_metadata

    提示

    一级 · 思路:这是”既要结构化结果、又要调用元数据”的官方做法 二级 · 方法model.with_structured_output(Movie, include_raw=True) 三级 · 骨架:打印完再试试故意让校验失败(比如 Schema 字段和问题完全不匹配),看 parsing_error 里装了什么

三、综合题#

  • 3-1 自己写一个 fake server,亲眼确认”只有 Pydantic 会校验” 分三步做:

    1. 写一个假服务端:监听 127.0.0.1:8889,收到 POST 后打印请求体里的工具名,然后固定返回一份字段名故意写错的 tool_calls 响应
    2. 客户端用 Pydantic 模式调用,观察抛出的 ValidationError(注意看错误信息里的 input_value
    3. 把 Schema 换成 TypedDict 再调一次,观察它把错误键名原样返回
    提示(先自己想,实在想不出再点开)

    一级 · 思路:假服务端只需要做三件事——读请求体、拿到工具名、拼一个 OpenAI 格式的响应 二级 · 方法http.server.BaseHTTPRequestHandler + ChatDeepSeek(api_base="http://localhost:8889") 三级 · 骨架:响应结构必须包含 choices[0].message.tool_calls[0].function.{name, arguments},其中 arguments字符串形式的 JSON(要 json.dumps

参考答案(做完再点开)
# ---------- fake_server.py ----------
import json
import time
from http.server import BaseHTTPRequestHandler, HTTPServer
# 故意把 title 写成 title1,并且不返回 year
FAKE_ARGS = {"title1": "盗梦空间", "director": "克里斯托弗·诺兰", "rating": 9.3}
class FakeDeepSeekHandler(BaseHTTPRequestHandler):
def do_POST(self):
content_length = int(self.headers.get("Content-Length", 0))
raw_body = self.rfile.read(content_length).decode("utf-8")
json_body = json.loads(raw_body)
print("[收到请求] tools[0].name =", json_body["tools"][0]["function"]["name"])
response = {
"id": "chatcmpl-test",
"object": "chat.completion",
"created": int(time.time()),
"model": "any",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "",
"tool_calls": [
{
"id": "call_1",
"type": "function",
"function": {
"name": json_body["tools"][0]["function"]["name"],
"arguments": json.dumps(FAKE_ARGS, ensure_ascii=False),
},
}
],
},
"finish_reason": "tool_calls",
}
],
"usage": {"prompt_tokens": 1, "completion_tokens": 1, "total_tokens": 2},
}
data = json.dumps(response, ensure_ascii=False).encode()
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(data)))
self.end_headers()
self.wfile.write(data)
def log_message(self, *args):
pass
if __name__ == "__main__":
HTTPServer(("127.0.0.1", 8889), FakeDeepSeekHandler).serve_forever()
# ---------- client.py(另开一个终端,先启动 fake_server 再运行它)----------
from langchain_deepseek import ChatDeepSeek
from pydantic import BaseModel, Field, SecretStr
from typing_extensions import Annotated, TypedDict
model = ChatDeepSeek(
model="deepseek-v4-flash",
api_base="http://localhost:8889",
api_key=SecretStr("<KEY>"),
)
# 第 2 步:Pydantic 模式 → 抛 ValidationError
class MovieModel(BaseModel):
"""电影的详细信息"""
title: str = Field(description="电影标题")
year: int = Field(description="电影上映年份")
director: str = Field(description="导演")
rating: float = Field(description="电影评分,满分十分")
try:
resp = model.with_structured_output(MovieModel).invoke("给出盗梦空间的信息")
print(resp, type(resp))
except Exception as e:
print("Pydantic 抛异常:", type(e).__name__)
print(str(e)[:200])
# 第 3 步:TypedDict 模式 → 原样返回错误键名
class MovieDict(TypedDict):
"""电影的详细信息"""
title: Annotated[str, ..., "电影标题"]
year: Annotated[int, ..., "电影上映年份"]
director: Annotated[str, ..., "导演"]
rating: Annotated[float, ..., "电影评分,满分十分"]
resp = model.with_structured_output(MovieDict).invoke("给出盗梦空间的信息")
print("TypedDict 返回:", resp, type(resp))
# {'title1': '盗梦空间', 'director': '克里斯托弗·诺兰', 'rating': 9.3} <class 'dict'>

评论区

[ 标签 ]
# AI37# AI 编程2# AI工具1# Ajax2# Apifox1# AstrBot3# Astro2# CC Switch1# CDN2# Claude Code1# claudecode2# ClaudeCode1# Cloudflare2# CloudFlare2# CloudFlare-ImgBed3# coc3# CSS6# DeepSeek6# deepseek2# DELETE1# Docker1# EdgeOne3# Gist1# git1# GitHub1# hexo-circle-of-friends1# HTML6# HTTP5# ImageManager1# Java23# java13# JavaScript5# JDBC3# JSON2# JUnit1# LangChain25# Logback1# Maven6# Muse Spark1# Mybatis1# MyBatis4# MySQL28# MySql1# NapCat1# Node.js1# obsidian2# Obsidian5# OpenCode4# ORM1# PathVariable1# PicGo1# PyCharm1# Python65# RequestBody1# RequestMapping1# RESTful风格1# skills1# Slf4j1# SpringBoot11# SQL2# Streamlit5# Svelte2# TailwindCSS1# Telegram3# Tlias2# Vercel1# vscode2# Vue7# Waline3# WebDAV1# Web基础6# Web开发6# WinSCP1# YAML1# 三层架构1# 中二宣言1# 书籍1# 使用文档10# 写作1# 函数2# 刷步数1# 前端32# 动态1# 动漫1# 包1# 单词2# 博客7# 博客工作流1# 博客开发2# 参数接收1# 友链1# 反思2# 图床6# 地图1# 备份2# 大模型1# 奇思妙想1# 存储1# 学习方法6# 学校1# 宝塔面板3# 宝宝10# 对象1# 导航栏1# 工具2# 开发1# 开发工具1# 开发规范1# 开心1# 异常处理1# 影视2# 微信1# 性能优化2# 总结1# 想法15# 感受1# 感悟11# 指南1# 提示词工程1# 插件5# 故障排除1# 效率工具2# 教程10# 数据分析9# 数据库27# 数据结构1# 文件操作2# 斩神1# 日常92# 日志框架1# 朋友圈1# 朱元璋1# 模块1# 模板1# 正则表达式2# 测试1# 游戏2# 爬虫7# 生活迁移1# 电影2# 电脑1# 碎碎念1# 视觉识别1# 类1# 类型注解1# 网络基础2# 网络教室1# 羊毛2# 脚本2# 脚本工具1# 自动化2# 蓝奏云1# 订阅推荐2# 记录2# 评论系统1# 词根1# 词缀1# 说说1# 足迹1# 跑步2# 路径参数1# 转载2# 运动1# 部落冲突1# 配置1# 随机图1# 面向对象5# 音乐3# 音标1# 饮食1# 驼峰命名1# 高德地图1
[ 公告 ]

如果你喜欢,那么欢迎来到我的世界!

了解更多
[ 音乐 ]
封面

音乐

暂未播放

0:000:00
暂无歌词
找不到相关结果。
[ contents ]
[ 全部文章 ]