大模型强制结构化输出指南(Instructor + LiteLLM/LangChain/LangGraph)
·
结构化输出:Instructor + LiteLLM,以及 LangChain/LangGraph 方案
本文目标是说明如何让大模型强制输出指定 JSON 结构(Schema),并给出在不同框架下的具体示例。
一、Instructor + LiteLLM:让模型“强制输出指定 JSON 结构”
1) Instructor 在这里做了什么
Instructor 的核心能力是把一次普通 LLM 调用变成“带结构化约束的调用”,通常包含这几个环节:
- Schema 注入:把你定义的结构(常见为 Pydantic 模型)以合适形式注入提示词/工具定义,让模型明确必须产出哪些字段、字段类型、必填项、枚举范围等。
- 解析与校验:拿到模型原始输出后做 JSON 解析,并用 Pydantic 校验类型与约束。
- 失败自动重试:校验失败时,基于失败原因引导模型修正输出;最多重试
max_retries次。 - 返回强类型对象:成功后直接返回
response_model的实例,而不是纯文本或不受约束的 dict。
“强制”的关键不在于一句“请输出 JSON”,而在于输出 → 解析 → 校验 → 失败重试的闭环。
2) LiteLLM 在这里做了什么
LiteLLM 的价值是用统一接口调用多厂商模型。结合 from_litellm()(instructor 的适配器)后:
- 你依旧用 LiteLLM 的参数风格组织
kwargs(如model、messages、temperature等)。 - Instructor 接管
response_model等结构化参数:负责约束输出、解析校验、失败重试,并把最终结果转换成 Schema 对象。
3) 典型调用方式(与你贴的片段对应)
你贴的逻辑本质是:
kwargs["response_model"] = RouteSchema:指定返回必须符合RouteSchemakwargs["max_retries"] = 3:最多自动修复 3 次result: RouteSchema = client.chat.completions.create(**kwargs):返回值直接是RouteSchema实例
4) RouteSchema 示例(Pydantic)
from typing import Literal, Optional
from pydantic import BaseModel, Field
class RouteSchema(BaseModel):
tool: Literal["search", "filesystem", "shell", "none"] = Field(..., description="要走的工具")
reason: str = Field(..., description="为什么这么路由(给人看的简短理由)")
query: Optional[str] = Field(None, description="如果 tool=search,需要搜索的关键词")
二、用 LangChain 或 LangGraph 能否实现同样效果?
可以。两者都能实现“强制输出指定 JSON 结构”,典型链路是:
定义结构(Pydantic/TypedDict) → 结构化输出/工具调用约束 → 解析校验失败自动修复或重试。
三、LangChain:结构化输出具体示例
示例 A:with_structured_output(Pydantic)(最接近 instructor 的体验)
from typing import Literal, Optional
from pydantic import BaseModel, Field
from langchain_openai import ChatOpenAI
class RouteSchema(BaseModel):
tool: Literal["search", "filesystem", "shell", "none"] = Field(..., description="路由到哪个工具")
reason: str = Field(..., description="简短理由")
query: Optional[str] = Field(None, description="tool=search 时的关键词")
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
# 关键:让 LLM 直接“返回 RouteSchema”
structured_llm = llm.with_structured_output(RouteSchema)
result: RouteSchema = structured_llm.invoke(
"我需要查看当前目录有什么文件,应该调用哪个工具?"
)
print(result.tool, result.reason, result.query)
说明:result 是强类型对象;底层通常依赖工具调用/结构化输出协议并做校验。
示例 B:解析/校验失败自动修复(类似“失败就再试一次”)
from typing import Literal, Optional
from pydantic import BaseModel
from langchain_openai import ChatOpenAI
from langchain.output_parsers import PydanticOutputParser, OutputFixingParser
from langchain_core.prompts import ChatPromptTemplate
class RouteSchema(BaseModel):
tool: Literal["search", "filesystem", "shell", "none"]
reason: str
query: Optional[str] = None
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
parser = PydanticOutputParser(pydantic_object=RouteSchema)
fixing_parser = OutputFixingParser.from_llm(parser=parser, llm=llm)
prompt = ChatPromptTemplate.from_messages([
("system", "你是路由器,只能输出符合格式的 JSON。"),
("human", "{question}\n\n{format_instructions}"),
])
chain = prompt | llm | fixing_parser
result: RouteSchema = chain.invoke({
"question": "我想读取 d:/github 下的文件列表,应该用哪个工具?",
"format_instructions": parser.get_format_instructions(),
})
print(result)
四、LangGraph:在“图”里强制结构化输出并用于路由
LangGraph 的优势是:结构化输出不仅是“拿到 JSON”,还能直接用它来决定下一步节点(非常适合 router 场景)。
from typing import Literal, Optional, TypedDict
from pydantic import BaseModel, Field
from langchain_openai import ChatOpenAI
from langgraph.graph import StateGraph, END
class RouteSchema(BaseModel):
tool: Literal["search", "filesystem", "shell", "none"] = Field(...)
reason: str = Field(...)
query: Optional[str] = None
class State(TypedDict, total=False):
user_input: str
route: RouteSchema
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
router_llm = llm.with_structured_output(RouteSchema)
def router_node(state: State) -> State:
route: RouteSchema = router_llm.invoke(
f"根据用户需求选择 tool,并给出 reason。\n用户:{state['user_input']}"
)
return {"route": route}
def search_node(state: State) -> State:
# 这里写你的 search 工具调用逻辑
return {"user_input": state["user_input"]}
def shell_node(state: State) -> State:
# 这里写你的 shell 工具调用逻辑
return {"user_input": state["user_input"]}
def none_node(state: State) -> State:
return {}
def route_selector(state: State) -> str:
return state["route"].tool
g = StateGraph(State)
g.add_node("router", router_node)
g.add_node("search", search_node)
g.add_node("shell", shell_node)
g.add_node("none", none_node)
g.set_entry_point("router")
g.add_conditional_edges(
"router",
route_selector,
{
"search": "search",
"shell": "shell",
"none": "none",
"filesystem": END, # 示例:你也可以加 filesystem 节点
},
)
g.add_edge("search", END)
g.add_edge("shell", END)
g.add_edge("none", END)
app = g.compile()
final_state = app.invoke({"user_input": "帮我列出当前目录文件"})
print(final_state["route"])
五、可靠性建议与常见坑(适用于三种方案)
- 用强约束类型提升一次通过率:
Literal/Enum、范围约束、必填字段等。 - Schema 不要太深太复杂:嵌套太深会降低稳定性、提升重试概率。
- 保留
reason字段:方便调试,也更利于失败重试时纠偏。 - 合理设置重试:一般 2~5 次足够,过大带来延迟与成本增加。
- 避免提示词冲突:例如同时要求“输出 Markdown 解释过程”与“只能输出 JSON”,容易导致解析失败。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)