课程0基础Agent开发课 / 工具使用与Function-Calling / LangChain-OutputParser结构化输出解析
— 17 min read

LangChain-OutputParser结构化输出解析

LLM 返回的永远是字符串。但程序要的是结构化数据——Python 的 dict(字典)、list(列表)、Pydantic 对象(带类型约束的数据类),可以直接操作、直接传给下游函数的东西。

LangChain OutputParser:结构化输出解析

LLM 返回的永远是字符串。但程序要的是结构化数据——Python 的 dict(字典)、list(列表)、Pydantic 对象(带类型约束的数据类),可以直接操作、直接传给下游函数的东西。

OutputParser 解决的就是这个从"字符串"到"结构化对象"的转换问题。

1.1 为什么不直接用 json.loads()

解析失败 → 重试

Parser 类型

StrOutputParser
直接返回字符串

JsonOutputParser
解析JSON对象

PydanticOutputParser
类型验证模型

CommaSeparatedListParser
逗号分隔列表

LLM
原始输出

Format Instructions
格式指令

OutputParser
解析处理

Python对象
Pydantic/Dict

下游
应用逻辑

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,模型可能返回 yearsexperience_years 或者直接不给这个字段。

手工解析一次还行,写十次就是重复劳动。更麻烦的是,手工解析代码非常脆——模型输出稍微变一点,就可能整段崩掉,而且崩掉的方式还各不相同,调试起来很痛苦。

OutputParser 把这些脏活包装起来,让你只管定义"我要什么结构",不用操心"怎么从字符串里挖出来"。

1.2 三种主要 OutputParser

LangChain 提供了从简单到复杂的 OutputParser,覆盖不同场景。

1.2.1 StrOutputParser:最简单的场景

直接把模型返回的 AIMessage 对象转成字符串:

python
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 系统..."

不加 StrOutputParserllm.invoke() 返回的是 AIMessage 对象(包含消息内容、类型等多个字段),要用 .content 取内容。加了 StrOutputParser,直接就是字符串。简单场景这就够了。

1.2.2 JsonOutputParser:解析 JSON 输出

需要 JSON 格式输出时用这个。它能自动从模型输出里提取 JSON 部分,就算模型在前后加了废话,也能解析出来:

python
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 会直接把模型输出解析成对应的对象,带完整的类型校验:

python
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_experienceresult["years_of_experience"] 更安全

1.3 现代方式:with_structured_output(推荐)

PydanticOutputParser 的工作原理是:在 Prompt 里塞格式说明,然后解析模型的文本输出。本质上是在"说服"模型按格式输出,不是强制的——模型仍然可能不按格式来。

现代 LLM 提供了更强的方式:结构化输出(Structured Output),直接在 API 层面要求模型输出符合 Schema 的 JSON,完全不走文本解析这条路。

python
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:

python
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 一个完整的实际应用:简历批量筛选

把上面所有东西组合起来,写一个能批量处理简历的函数:

python
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)

预期输出:

code
[过滤] 张三:年限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 了——代码更少,问题更少。

本页目录