拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

DeepSeek-Agent-Harness-2026终极指南-第3章第15节-API协议内幕-结构化输出:让模型说机器话

DeepSeek-Agent-Harness-2026终极指南-第3章第15节-API协议内幕-结构化输出:让模型说机器话

结构化输出:让模型说"机器话"

你让模型输出 JSON,它偏偏在前面加一句"好的,以下是您要的JSON:“,后面再包三层 markdown 代码块。这不是模型叛逆,是你没跟它"约法三章”。这节搭一套三层防御体系,让模型输出变成机器可信赖的数据。

本文导航

  • 问题的根源:模型是话痨
  • 第一层防御:JSON Output 模式
  • 第二层防御:pydantic 校验
  • 第三层防御:自动重试
  • 三层合体:生产级输出管线
  • 防御性编程的哲学
  • 小结
  • 下节预告

第 3 章的收官节。前面我们搞定了协议(第 10 节)、数据结构(第 11 节)、能力全景(第 12 节)、成本(第 13 节)、流式(第 14 节)——还剩最后一块拼图:怎么让模型的输出被程序安全地消费。

为什么这块拼图这么重要?看一个 Agent 的典型工作流:模型分析完任务,输出一个"行动计划"——下一步调什么工具、参数是什么。这个计划要被你的代码解析执行。如果模型输出的是自然语言包裹的 JSON,你的解析代码就永远活在"今天它又换了个包装"的恐惧里。

Agent 的世界里,模型输出 = 程序输入。这个接口不稳,整个系统就是沙地上盖楼。

问题的根源:模型是话痨

先直面问题的根源。模型的本质是什么?一个被海量自然语言训练出来的、以"说人话"为本能的生成器。它的整个训练生涯都在学"怎么把话说得像话"——所以让它输出一个赤裸裸的 JSON,它会本能地:

  1. 加开场白:“好的,以下是您需要的JSON:”——礼貌,但致命
  2. 包 markdown:` ```json … ````——它觉得这样"更专业"
  3. 补注释:JSON 里塞// 注释(非法语法)
  4. 截尾:输出到一半被 max_tokens 掐了,还欠一个右括号
  5. 自由发挥:字段名多了少了、类型飘了("age": "25"字符串)

你写json.loads(model_output)撞上其中任何一条,就是一场异常。别怪模型,它只是做了它这辈子最擅长的事——说人话。是我们的问题:没跟它约法三章。

约法三章就是三层防御,一层层看。

第一层防御:JSON Output 模式

第一层是官方兜底:JSON Output 模式。第 12 节露过面,这次展开讲透。

resp=client.chat.completions.create(model="deepseek-flash",messages=[{"role":"system","content":"你是 JSON 生成器,只输出 JSON。"},# 约束要写进 system{"role":"user","content":"生成一个虚构的用户:name, age, email"},],response_format={"type":"json_object"},# 官方开关:强制 JSON)print(resp.choices[0].message.content)
$ uv run python json_mode.py {"name": "林小满", "age": 28, "email": "linxiaoman@example.com"}

response_format={"type": "json_object"}这个开关让服务端用受约束的解码保证输出是合法 JSON——技术上,模型每生成一个 token 时都被限制在"JSON 语法允许的下一个字符"里选择,语法上不可能跑出合法 JSON 的边界。这是三层里最硬的保证。

但两个使用条件必须遵守,否则直接报错:

  1. system 或 user 消息里必须包含 “json” 这个词(提示模型输出 JSON),不然 API 会拒绝请求
  2. 它只保证"语法合法",不保证"结构符合"——字段名对不对、类型对不对、有没有缺字段,它不管。模型完全可能给你{"username": "林小满"}而你要的是name

第二个条件就是第二层防御存在的原因。

第二层防御:pydantic 校验

JSON Output 保证了"话是机器话",但没保证"说的是你要的话"。结构校验,交给 pydantic——这也是"五条铁律"里"JSON 处理一律 pydantic"的主场。

用 pydantic 定义你要的"合同":

frompydanticimportBaseModel,EmailStr,FieldclassUser(BaseModel):name:str=Field(min_length=1)# 不许空名字age:int=Field(ge=0,le=150)# 0~150,防"25岁"字符串或-3email:EmailStr# 邮箱格式强校验

然后一行验收:

user=User.model_validate_json(raw_json)# 解析+类型转换+约束,一气呵成

这行代码替你干了四件事:解析 JSON、类型转换("25"→25,如果模型给了字符串的话)、范围校验(age 不可能是 999)、格式校验(email 必须像邮箱)。任何一关不过,抛出带字段路径的详细错误——age: Input should be less than or equal to 150,而不是json.loads那句没头没尾的Expecting value。

更妙的是,pydantic 还是双向的。校验模型输出只是半程,你还可以用User.model_json_schema()反向生成 JSON Schema 塞进提示词,让模型照着表格填——定义和校验共用同一份真相,永远不会再"提示词说一套、校验查一套":

importjson schema=User.model_json_schema()prompt=f"生成一个虚构用户,严格遵守此 JSON Schema:\n{json.dumps(schema)}"

这个"pydantic 模型即合同"的模式,会成为 DeepPilot 全项目的通用范式——从工具参数校验(第 21 节)、到子 Agent 结果交接(第 12 章)、到 MCP 的 Schema 生成(第 13 章),全用它。

第三层防御:自动重试

有前两层,失败率已经很低,但不是零。剩下的长尾怎么办?——重试,但不是无脑重试。

结构化输出的重试有个黄金技巧:把校验错误喂回去。模型第一次输出不合格,不要默默重跑(它可能再犯一模一样的错),要把"你哪里错了"明确告诉它:

渲染错误:Mermaid 渲染失败: Parse error on line 4: ...| D[把错误信息追加进对话
"age 必须是 0-150 的整数,你给... -----------------------^ Expecting 'SQE', 'DOUBLECIRCLEEND', 'PE', '-)', 'STADIUMEND', 'SUBROUTINEEND', 'PIPE', 'CYLINDEREND', 'DIAMOND_STOP', 'TAGEND', 'TRAPEND', 'INVTRAPEND', 'UNICODE_TEXT', 'TEXT', 'TAGSTART', got 'STR'

实践证明这一招的修复率极高——因为模型"看得到"自己错在哪,改起来又快又准。统计上,第二轮修复率通常在九成以上,三轮内基本收敛。

defask_user_json(client,max_retries=3):msgs=[{"role":"system","content":f"生成用户,严格遵守:{User.model_json_schema()}"},]foriinrange(max_retries):raw=client.chat.completions.create(model="deepseek-flash",messages=msgs,response_format={"type":"json_object"},).choices[0].message.contenttry:returnUser.model_validate_json(raw)# 校验通过,直接交付exceptExceptionase:msgs.append({"role":"assistant","content":raw})# 它上次的答案msgs.append({"role":"user","content":f"输出不合规:{e}\n请修正后重新输出 JSON。"})raiseRuntimeError("结构化输出重试耗尽")# 降级交给上层

注意重试上限(3 次)——重试不是无限次的,耗尽就走降级(记留痕、报警、走人工分支)。这个"有限重试 + 优雅降级"的骨架,第 19 节会扩展成整个 DeepPilot 的容错体系。

三层合体:生产级输出管线

把三层叠起来,就是一个可以直接搬进 DeepPilot 的完整函数。给它一个正经的定位:这是模型输出与你的程序之间的海关,所有越境数据必须过检:

"""structured.py —— 三层防御的结构化输出管线(需 DEEPSEEK_API_KEY)。"""importosfromopenaiimportOpenAIfrompydanticimportBaseModel,EmailStr,Field client=OpenAI(base_url="https://api.deepseek.com",api_key=os.environ["DEEPSEEK_API_KEY"])classUser(BaseModel):"""合同:字段即约束,注释即文档。"""name:str=Field(min_length=1)age:int=Field(ge=0,le=150)email:EmailStrdefask_structured(prompt:str,model_cls:type[BaseModel],max_retries:int=3)->BaseModel:"""生产级管线:JSON Output(语法) + pydantic(结构) + 错误回传重试(长尾)。"""schema=model_cls.model_json_schema()msgs=[{"role":"system","content":f"只输出 JSON,严格遵守此 Schema:{schema}"},{"role":"user","content":prompt}]forattemptinrange(1,max_retries+1):raw=client.chat.completions.create(model="deepseek-flash",messages=msgs,response_format={"type":"json_object"},# 第1层:语法保证).choices[0].message.contenttry:returnmodel_cls.model_validate_json(raw)# 第2层:结构校验exceptExceptionase:print(f"[第{attempt}次校验失败]{e.__class__.__name__}:{str(e)[:60]}")msgs+=[{"role":"assistant","content":raw},{"role":"user","content":f"输出违规:{e}。请修正后重新输出。"}]# 第3层raiseRuntimeError(f"{model_cls.__name__}结构化输出重试耗尽")if__name__=="__main__":u=ask_structured("生成一个虚构的用户信息",User)print(f"验收通过:{u.name}/{u.age}/{u.email}")
$ uv run python structured.py 验收通过: 林小满 / 28 / linxiaoman@example.com

这段代码 40 行不到,但每一层都在各自的防区干活:语法层挡掉包装和截断,结构层挡掉字段漂移,重试层消化长尾。从此你的下游代码拿到的都是验过货的强类型对象,解析这个词从你的字典里消失了。

防御性编程的哲学

这节背后其实是一个贯穿整个课程的哲学:模型输出永远不可信,但永远可以被校验。

跟第 6 节的"模型永远不可信,但永远可以被约束"对上暗号了:

  • 第 6 节说的是能力边界:模型不能直接执行,必须过 Harness 的手(工具系统、权限)
  • 本节说的是数据边界:模型输出不能直接用,必须过校验的海关(pydantic)

两句话合起来,就是 Agent 工程的世界观:模型是个聪明但不稳定的员工,Harness 是一套让它发光、又让它不闯祸的制度。后面你写 Agent Loop、工具执行、子 Agent 交接,会一次次遇到"这里要不要信任模型的输出"——默认答案是:不要,加校验。校验的成本永远低于出错后的排查成本。

小结

  1. 模型输出 = 程序输入,这个接口必须稳,否则系统是沙地盖楼;模型"话痨"是本能不是 bug。
  2. 三层防御:JSON Output 保语法(受约束解码)、pydantic 保结构(类型+范围+格式)、错误回传重试保长尾(把校验错误喂回模型,修复率极高)。
  3. pydantic 模型即合同:model_json_schema()进提示词、model_validate_json()验输出,定义与校验共享同一份真相。
  4. 重试要有限度:3 次封顶,耗尽走降级(留痕+人工分支),无限重试是事故放大器。
  5. 世界观收口:能力边界靠约束(工具/权限),数据边界靠校验(pydantic)——第 3 章理论筑基,到此全线贯通。

下节预告

理论筑基区(第 3~5 章)的第 3 章完结。但真正的重头戏从下一节开始——第 4 章 Agent Loop:智能体的心脏。第 16 节先上思想课:ReAct 循环:推理与行动的交替之舞。Thought-Action-Observation 三段式怎么运转、一个完整的 ReAct 对话长什么样(我会给你一段带真实控制台输出的模拟)、为什么这个 2022 年的论文思想至今统治所有 Agent 产品。从下一节开始,你离"亲手写出 DeepPilot"只剩一步。


如果觉得本文对你有帮助,欢迎点赞、收藏、关注三连!
本系列持续更新中,80篇硬核实战,关注不迷路~

返回列表