LangChain-OutputParser结构化输出解析
LLM 返回的永远是字符串。但程序要的是结构化数据——Python 的 dict(字典)、list(列表)、Pydantic 对象(带类型约束的数据类),可以直接操作、直接传给下游函数的东西。
LangChain OutputParser:结构化输出解析
LLM 返回的永远是字符串。但程序要的是结构化数据——Python 的 dict(字典)、list(列表)、Pydantic 对象(带类型约束的数据类),可以直接操作、直接传给下游函数的东西。
OutputParser 解决的就是这个从"字符串"到"结构化对象"的转换问题。
1.1 为什么不直接用 json.loads()
OutputParser 工作流程——LLM 原始文本经四种 Parser 转换为结构化 Python 对象
使用方式:chain = prompt | llm | parser | parser.get_format_instructions() 注入到Prompt
最朴素的想法:在 Prompt 里说"请以 JSON 格式返回",然后用 json.loads() 解析,不就行了?
大部分时候确实能跑。但这套方案有几个隐患:
模型喜欢加废话:即便你说"只输出 JSON,不要有其他内容",有些模型仍然会在 JSON 前后加上"好的,这是您要的 JSON:"之类的解释文字。json.loads() 遇到这种情况直接报错。
类型不一致:你期望 age 是整数 25,模型可能返回字符串 "25"。你期望 skills 是列表 ["Java", "Python"],模型可能返回字符串 "Java, Python"。
字段缺失或命名变形:你定义了 years_of_experience,模型可能返回 years、experience_years 或者直接不给这个字段。
手工解析一次还行,写十次就是重复劳动。更麻烦的是,手工解析代码非常脆——模型输出稍微变一点,就可能整段崩掉,而且崩掉的方式还各不相同,调试起来很痛苦。
OutputParser 把这些脏活包装起来,让你只管定义"我要什么结构",不用操心"怎么从字符串里挖出来"。
1.2 三种主要 OutputParser
LangChain 提供了从简单到复杂的 OutputParser,覆盖不同场景。
1.2.1 StrOutputParser:最简单的场景
直接把模型返回的 AIMessage 对象转成字符串:
from langchain_core.output_parsers import StrOutputParser
from langchain_openai import ChatOpenAI
import os
llm = ChatOpenAI(
model="deepseek-chat",
api_key=os.getenv("DEEPSEEK_API_KEY"),
base_url="https://api.deepseek.com",
)
chain = llm | StrOutputParser()
result = chain.invoke("用一句话解释什么是 AI Agent")
print(type(result)) # <class 'str'>
print(result) # "AI Agent 是一种能够感知环境、做出决策并执行行动的 AI 系统..."
不加 StrOutputParser,llm.invoke() 返回的是 AIMessage 对象(包含消息内容、类型等多个字段),要用 .content 取内容。加了 StrOutputParser,直接就是字符串。简单场景这就够了。
1.2.2 JsonOutputParser:解析 JSON 输出
需要 JSON 格式输出时用这个。它能自动从模型输出里提取 JSON 部分,就算模型在前后加了废话,也能解析出来:
from langchain_core.output_parsers import JsonOutputParser
from langchain_core.prompts import ChatPromptTemplate
parser = JsonOutputParser()
prompt = ChatPromptTemplate.from_messages([
("system", "你是一个数据提取助手,请从用户提供的简历文本中提取结构化信息。"),
("human", "从以下简历中提取信息:\n{resume}\n\n{format_instructions}"),
])
chain = prompt | llm | parser
result = chain.invoke({
"resume": "张三,5年Java开发经验,熟悉Spring Boot、MySQL、Redis",
"format_instructions": parser.get_format_instructions(),
})
print(type(result)) # <class 'dict'>
print(result)
# {'name': '张三', 'years_of_experience': 5, 'skills': ['Spring Boot', 'MySQL', 'Redis']}
get_format_instructions() 返回一段提示文字,告诉模型输出 JSON 格式,这段文字会插进 Prompt 里。
JsonOutputParser 的问题:它返回的是普通 dict,没有字段校验。如果模型没给 name 字段,你用 result["name"] 会直接 KeyError。如果类型不对,也没有自动转换。需要更强的约束,就用下面这个。
1.2.3 PydanticOutputParser:带类型校验的结构化输出
最推荐的方式之一。先用 Pydantic 定义期望的数据结构,Parser 会直接把模型输出解析成对应的对象,带完整的类型校验:
from langchain_core.output_parsers import PydanticOutputParser
from pydantic import BaseModel, Field
from typing import List
# 定义期望的数据结构
class CandidateProfile(BaseModel):
name: str = Field(description="候选人姓名")
years_of_experience: int = Field(description="工作年限,整数")
skills: List[str] = Field(description="技能列表,每项是一个技能名称")
summary: str = Field(description="一句话总结这位候选人的核心亮点")
parser = PydanticOutputParser(pydantic_object=CandidateProfile)
prompt = ChatPromptTemplate.from_messages([
("system", "你是一个简历分析助手。"),
("human", "分析这份简历:\n{resume}\n\n{format_instructions}"),
])
chain = prompt | llm | parser
result = chain.invoke({
"resume": "李四,8年后端开发,精通 Go、Python、Kubernetes,曾在阿里担任技术专家",
"format_instructions": parser.get_format_instructions(),
})
print(type(result)) # <class 'CandidateProfile'>
print(result.name) # 李四
print(result.years_of_experience) # 8(整数,不是字符串)
print(result.skills) # ['Go', 'Python', 'Kubernetes']
print(result.summary) # "8年后端开发技术专家,精通云原生技术栈..."
Pydantic(Python 数据验证库,可以定义数据结构并自动检查字段类型)的好处:
- 如果模型返回的
years_of_experience是字符串"8"而不是整数8,Pydantic 会自动类型转换 - 字段缺失或类型完全不对,会抛出
ValidationError,而不是默默返回错误数据 - 有了 Pydantic 对象,IDE 有代码补全,
result.years_of_experience比result["years_of_experience"]更安全
1.3 现代方式:with_structured_output(推荐)
用 PydanticOutputParser 的工作原理是:在 Prompt 里塞格式说明,然后解析模型的文本输出。本质上是在"说服"模型按格式输出,不是强制的——模型仍然可能不按格式来。
现代 LLM 提供了更强的方式:结构化输出(Structured Output),直接在 API 层面要求模型输出符合 Schema 的 JSON,完全不走文本解析这条路。
from pydantic import BaseModel, Field
from typing import List
from langchain_openai import ChatOpenAI
import os
llm = ChatOpenAI(
model="deepseek-chat",
api_key=os.getenv("DEEPSEEK_API_KEY"),
base_url="https://api.deepseek.com",
)
class CandidateProfile(BaseModel):
name: str = Field(description="候选人姓名")
years_of_experience: int = Field(description="工作年限,整数")
skills: List[str] = Field(description="技能列表")
summary: str = Field(description="一句话总结候选人核心亮点")
# 直接让模型输出结构化数据,不需要 Prompt 里塞格式说明
structured_llm = llm.with_structured_output(CandidateProfile)
result = structured_llm.invoke(
"分析这份简历:王五,12年Java开发,精通Spring Cloud微服务、Kafka、Elasticsearch,"
"曾在字节担任高级架构师,主导过日均亿级请求系统的架构设计"
)
print(type(result)) # <class 'CandidateProfile'>
print(result.name) # 王五
print(result.years_of_experience) # 12
print(result.skills) # ['Spring Cloud', 'Kafka', 'Elasticsearch', ...]
print(result.summary) # 12年Java架构师,专注大规模分布式系统...
with_structured_output 对比 PydanticOutputParser 的优势:
- 不需要在 Prompt 里塞格式说明(
format_instructions) - 不走文本解析,在 API 层面强制约束,解析成功率接近 100%
- 代码更简洁,不需要手动构建
chain
如果你在用支持结构化输出的模型(DeepSeek、GPT-4o、Claude 等主流模型都支持),with_structured_output 是首选。
1.4 处理嵌套结构
真实业务数据往往不是扁平的,嵌套结构很常见。Pydantic 天然支持嵌套 model:
from pydantic import BaseModel, Field
from typing import List, Optional
class WorkExperience(BaseModel):
company: str = Field(description="公司名称")
position: str = Field(description="职位名称")
years: float = Field(description="在职年数,可以是小数如 2.5")
highlights: List[str] = Field(description="主要成就,最多3条,每条一句话")
class CandidateProfileDetailed(BaseModel):
name: str = Field(description="候选人姓名")
total_years: int = Field(description="总工作年限,整数")
skills: List[str] = Field(description="核心技能列表")
work_experiences: List[WorkExperience] = Field(description="工作经历列表,按时间倒序")
education: Optional[str] = Field(default=None, description="最高学历,如无则为 None")
structured_llm = llm.with_structured_output(CandidateProfileDetailed)
result = structured_llm.invoke(
"分析简历:赵六,前阿里P8,在阿里做了5年电商中台架构,"
"之前在腾讯做了3年游戏后端,本科计算机,精通Java/Go/分布式系统"
)
print(f"候选人:{result.name},总工作年限:{result.total_years}年")
for exp in result.work_experiences:
print(f"\n {exp.company} - {exp.position}({exp.years}年)")
for h in exp.highlights:
print(f" - {h}")
Optional[str] 表示这个字段可以为 None,适合简历里不一定有的信息(比如有些人不写学历)。嵌套多深都行,Pydantic 会递归校验每一层。
1.5 一个完整的实际应用:简历批量筛选
把上面所有东西组合起来,写一个能批量处理简历的函数:
from pydantic import BaseModel, Field, field_validator
from typing import List, Optional
from langchain_openai import ChatOpenAI
import os
llm = ChatOpenAI(
model="deepseek-chat",
api_key=os.getenv("DEEPSEEK_API_KEY"),
base_url="https://api.deepseek.com",
)
class ResumeAnalysis(BaseModel):
name: str = Field(description="候选人姓名")
total_years: int = Field(description="总工作年限,整数")
primary_skills: List[str] = Field(description="主要技能,最多5个")
education_level: str = Field(description="最高学历:本科/硕士/博士/其他")
highlight: str = Field(description="最核心的一句话亮点,20字以内")
score: int = Field(description="综合评分 1-10,10分最高")
@field_validator('score')
@classmethod
def score_must_be_valid(cls, v):
if not 1 <= v <= 10:
raise ValueError('评分必须在 1-10 之间')
return v
@field_validator('total_years')
@classmethod
def years_must_be_positive(cls, v):
if v < 0:
raise ValueError('工作年限不能为负数')
return v
# 创建结构化输出的 LLM
structured_llm = llm.with_structured_output(ResumeAnalysis)
def analyze_resume(resume_text: str) -> ResumeAnalysis:
"""分析一份简历,返回结构化评估结果"""
prompt = f"""请分析以下简历,给出结构化评估:
{resume_text}
评分标准:
- 技术深度:技能是否有深度,不只是会用
- 项目规模:是否有大规模系统经验
- 成长轨迹:职业发展是否清晰向上
"""
return structured_llm.invoke(prompt)
def batch_screen_resumes(resumes: List[str], min_years: int = 5, min_score: int = 7):
"""批量筛选简历"""
results = []
for i, resume in enumerate(resumes):
try:
analysis = analyze_resume(resume)
if analysis.total_years >= min_years and analysis.score >= min_score:
results.append(analysis)
print(f"[通过] {analysis.name}:{analysis.highlight}({analysis.score}/10)")
else:
print(f"[过滤] {analysis.name}:年限{analysis.total_years}年,评分{analysis.score}/10")
except Exception as e:
print(f"[错误] 第{i+1}份简历解析失败:{e}")
print(f"\n共 {len(results)}/{len(resumes)} 份简历通过筛选")
return sorted(results, key=lambda x: x.score, reverse=True)
# 测试
resumes = [
"张三,3年Java开发,熟悉Spring Boot,做过电商系统",
"李四,8年后端开发,精通分布式系统、Kafka、Elasticsearch,前阿里P7,主导过日均千万请求的系统设计",
"王五,6年Python开发,熟悉机器学习,在初创公司担任技术负责人,带领5人团队从0到1构建推荐系统",
]
top_candidates = batch_screen_resumes(resumes)
预期输出:
[过滤] 张三:年限3年,评分5/10
[通过] 李四:阿里P7,大规模分布式系统专家(9/10)
[通过] 王五:Python+ML全栈技术负责人(7/10)
共 2/3 份简历通过筛选
1.6 对比总结:什么时候用哪个
| 方式 | 何时使用 | 优点 | 缺点 |
|---|---|---|---|
StrOutputParser |
只需要文本输出 | 最简单 | 没有结构化 |
JsonOutputParser |
简单 JSON,不需要类型校验 | 比手工解析稳 | 无类型约束 |
PydanticOutputParser |
需要类型校验,模型不支持 structured output | 有完整类型校验 | 仍依赖 Prompt,偶尔失败 |
with_structured_output |
主流模型(DeepSeek、GPT-4o、Claude 等) | 最可靠,最简洁 | 需要模型支持 |
结论:如果你用的是主流商业模型,直接用 with_structured_output。它不依赖模型"自觉"按格式输出,而是在 API 层面强制约束,解析成功率接近 100%,代码也最简洁。
如果还在 Prompt 里写"请以 JSON 格式输出,格式如下……"然后手写 json.loads() 解析,可以换到 with_structured_output 了——代码更少,问题更少。