当前位置: 首页 > news >正文

面向 LLM 的程序设计 10:链式任务中的中间输出格式——如何写提示才能稳定得到可解析结构

如果你读过第 6 篇工具调用完整生命周期,应该对整条链路已有清晰理解。接下来你会遇到一类新场景:模型生成的内容不是给用户阅读,而是供下游代码直接消费

打个比方:工厂流水线上,上一道工序必须按固定规格输出零件,下一道才能顺利接手。链式任务中,LLM 每个节点的输出就是这道工序的交付物——规格不对,整条流水线都要停摆。

为什么要专门讨论「中间输出」?

实际场景中,模型常需生成一些"中间状态"——如路由标签、槽位信息、子任务列表、检索 query、JSON 格式的执行计划等。这些内容不会展示给最终用户,却要被json.loads()解析、被条件分支判断、被下游节点读取。中间一步格式出错,就像传话游戏传错一个字,后面全部乱套。

本文是系列第十篇,探讨在多步推理、Prompt 链、LangGraph 节点之间,如何将中间输出约定为稳定的结构化格式:定义字段、类型、缺值处理方式,在提示词中给出正反示例,并与解析、校验、重试机制配合。

核心要点:稳定链式结构的关键是「一步一契约、失败就地修复」。

  • 每个节点只输出单一封装格式(如单层 JSON 或带 XML 标签的 JSON)
  • 必填字段精简明确;缺数据时使用unknown/none等团队约定的哨兵值,而非空字符串
  • 提示词中提供最小正例 + 边界反例
  • 解析失败时优先在当前节点重试或降级处理

这些做法与第 6 篇的工具调用参数构造原则一致:单块输出、优先取最后一个合法 JSON、失败时将错误信息回注再生成。

关键词:Prompt Chaining;LangGraph;中间表示;可解析输出;JSON;节点契约;重试

案例代码链接:Langgragh 2. 路由 Routing 源代码。

0 系列回顾

  • 面向 LLM 的程序设计 1:API 契约设计:从 REST 到「能力端点」。能力化端点:为具体业务动作各自暴露的专用接口,例如/summarize-document/list-orders-by-user;不要把所有需求都丢进一个万能/ask接口。
  • 面向 LLM 的程序设计 2:确定性契约:为什么 LLM 调用的 API 需要严格 JSON Schema。用 JSON Schema 钉死类型、枚举与必填,对冲模型输出的随机性,减少歧义与解析失败。
  • 面向 LLM 的程序设计 3:LLM-Friendly 的响应结构:扁平键、稳定字段与类型标注。键名稳定、结构尽量扁平、语义一眼可读,方便模型与下游工具链消费。
  • 面向 LLM 的程序设计 4:API 版本化与演进——在「模型会记忆旧文档」前提下的兼容策略。显式版本、可渐进扩展与废弃公告,避免模型仍按旧文档调用已变更接口。
  • 面向 LLM 的程序设计 6:Tool Calling 的完整生命周期——从定义、决策、执行到观测回注。从工具定义到回注再推理串成闭环,每步可校验、可观测、失败可处理。
  • 面向 LLM 的程序设计 7:工具描述的工程化——name、description、parameters 怎么写才少误用。稳定 name、写清何时用与边界、Schema 与文案一致,降低选错工具与填错参的概率。
  • 面向 LLM 的程序设计 8:「少而宽」还是「多而窄」——工具粒度与 Token 预算的权衡。在工具个数、单工具覆盖面与上下文占用之间做工程权衡,平衡误触率与 Token 成本。
  • 面向 LLM 的程序设计 9:系统提示中的「能力边界」——减少越权与幻觉调用。在系统提示里划清能做与不能做,减少越权操作与「假装能调」的幻觉调用。

1 什么是「中间输出」?

简单说:不是写给用户读的优美段落,而是写给程序读的结构化清单

常见的几种形态:

  • 路由节点:输出intenttopicnext_node,供条件边判断「下一步走向哪个节点」。
  • 规划节点:输出步骤清单steps: [{id, action, inputs}]
  • 检索节点:输出queries(字符串数组)和filters,直接传给向量库 API。
  • 校验节点:输出ok: true/false和问题详情issues: [{code, field}, ...]

💡理解要点:这些内容通常对最终用户不可见(或仅在日志中可见)。既然是机器消费,就要比聊天回复更死板、更规整——闲聊可以模糊,中间数据必须精确。


2 设计原则:一步一契约

把每个节点看作对外签署的一份简明合约:我只认这几个字段,多一个少一个都不行。

2.1 单一信封格式

如果模型先解释一大段再塞 JSON,解析器很容易截错。更稳妥的做法是让每个节点只输出一块内容,程序从固定位置提取即可。例如:

  • 使用 Markdown 的JSON 代码围栏(标记为json,内部是单层{ ... }对象);
  • 或包裹在<router_output>…</router_output>标签内,里面是 JSON。

如果业务场景无法阻止模型「先解释后 JSON」,解析时应采纳最后一个合法 JSON 块(这与常见路由/工具 JSON 的解析策略一致)。

2.2 字段精简、语义明确、取值可枚举

为什么?字段越多,模型越容易自由发挥。下游若宽松放行,脏数据会静默下传;若严格校验,每一步都可能报错。

  • intent使用固定枚举(如refund | track | cancel),不要用整句自然语言描述意图。
  • 置信度如需表达,使用high | medium | low比 0.37 这类小数更稳定(减少合法写法种类,降低出错概率)。
  • 列表设置上限(如 query 最多 5 条),避免单次响应耗尽 token。

🔍实际示例:路由节点可以约定如下格式——

{"intent":"refund","confidence":"high","slots":{"order_id":"ORD-100001"}}

intent必须在白名单内;slots仅包含当前 intent 必需的字段,不要顺手塞入「可能有用」的冗余信息。

2.3 显式表达「无/未知」

为什么?程序读取 JSON 时,""(空字符串)与「字段缺失」难以区分——到底是模型判定无此数据,还是漏写了?

与团队约定统一哨兵值,使用unknownnonenull之一表达「缺失或不确定」。不要用空字符串或省略字段来糊弄。


3 如何写提示词,才能避免像布置作文题?

目标是让模型像填写表格,而非像撰写周记

3.1 可复用的小模板(每个节点四段式)

建议每个节点的提示词都包含以下四部分(可复制模板后修改名词):

  1. 角色定义:你是流水线中的「某某节点」,不要与用户寒暄,不要输出免责声明占用篇幅。
  2. 输出规格:逐条列出字段名、数据类型、枚举值、是否必填——这就是该节点的「字段清单」。
  3. 示例:提供一个最短正确示例;再提供一个信息不足时的示例(如槽位填unknown)。
  4. 禁止事项:不要输出清单外的字段;不要用 Markdown 表格代替 JSON(表格对程序不友好)。

💡理解要点:提示词中多写两行示例,往往比事后在日志中排查格式错误、再调整解析器更节省总体成本

3.2 与 LangGraph 的状态字段对齐

为什么?多一层「JSON 键 → 内存字段」的手工映射,迟早有人在某次修改时漏掉一边。

如果代码中使用state["router"],提示词中 JSON 的顶层键最好直接对应router所需的字段名(或采用团队统一命名)。提示词中的键 = 状态机中的键,减少隐式约定。

结构化 intent+slots

queries+filters

用户输入

节点_Router

节点_Retrieve

节点_Answer

3.3 一个案例

背景:同一套对话入口里,用户问题常常要走向不同处理链路——例如有的与预订机票/酒店相关,有的属于一般信息问答,还有的表述含糊,不宜直接进重业务分支。若不做显式分流,要么所有请求挤在一条链上难以维护,要么在代码里堆叠大量 if-else,路由规则与业务逻辑缠在一起。

目的:用LangGraph搭一个最小可运行图:先用 LLM 做一步意图归类,把结果写入共享状态里的离散字段再用条件边按该字段把请求分发到不同节点。这样可以把「判意图」与「办业务」拆开。本例要强调的是:字段decision取值为booker | info | unclear,它是给程序与条件边消费的中间契约,不是面向最终用户的回答正文;路由节点里还对模型输出做规范化与白名单校验,非法时降级为unclear,对应前文「一步一契约、失败就地修复」里的节点内校验与降级

示例实现位于本仓库Agent/Agentic_Design_Patterns_Langgraph/2_Routing/demo_codes/routing_graph.py(同目录可按requirements.txt安装依赖、配置 API Key 后运行main.py)。

3.3.1 架构(状态图 + 条件边)
  • 状态(State)RoutingState在节点间共享,关键中间字段为decisionbooker | info | unclear)。
  • 路由节点router:只负责根据用户请求写入decision,不直接调用下游业务节点。
  • 路由函数route_by_decision:供条件边使用,读取state["decision"],返回下一节点名(booking/info/unclear)。
  • 条件边:从router出发,按路由函数返回值映射到三条分支之一。
  • 处理节点bookinginfounclear各自生成最终response,再连到END

booker

info

unclear

START

router
LLM 意图分类
写入 decision

route_by_decision
读 state decision

booking
预订类处理

info
信息类处理

unclear
未识别兜底

END

整体路径:START → router →(条件边)→ booking / info / unclear → END。若你希望中间契约从「单 token」升级为JSON,只需在node_router内改为解析 JSON 并写入state中对应字段;图结构(router + 条件边 + 多分支)可保持不变

3.3.2 各节点(及路由函数)职责
名称类型输入(主要读取)输出(写入 state)说明
router图节点requestdecision调用prompt | llm | StrOutputParser,要求模型只输出booker/info/unclear;对输出做strip().lower()与白名单校验,非法值降级为unclear——即节点内完成「解析 + 校验 + 降级」。
route_by_decision路由函数(非 LLM 节点)decision无(返回下一节点名字符串)条件边的「选路」逻辑:与decision枚举对齐,缺省或异常走向unclear分支。
booking图节点requestresponse模拟预订机票/酒店类请求的处理结果(示例中为占位文案)。
info图节点requestresponse模拟一般信息类请求的处理结果。
unclear图节点requestresponse无法归类时的兜底回复,引导用户补充意图;与路由校验失败时的unclear决策一致。

辅助函数:_build_llm从配置构造ChatOpenAI_router_chain组装ChatPromptTemplate+ LLM +StrOutputParserbuild_routing_graph注册节点与边并compile()

3.3.3 如何运行(与 Demo 目录 README 一致)

Agent/Agentic_Design_Patterns_Langgraph/2_Routing/demo_codes下创建虚拟环境,执行pip install -r requirements.txt,配置.env(如OPENAI_API_KEYDASHSCOPE_API_KEY,可选BASE_URLMODEL),然后运行python main.py,可对示例请求观察decision与各分支response

3.3.4 完整源代码(routing_graph.py
""" LangGraph Routing 示例:基于意图的路由图。 流程:用户请求 → [路由节点:LLM 分类] → 条件边 → [预订/信息/未识别] 处理节点 → 响应 运行前请配置环境变量 OPENAI_API_KEY 或 DASHSCOPE_API_KEY,或在项目根目录放置 .env 文件。 """importosfromtypingimportLiteral,TypedDictfromlangchain_openaiimportChatOpenAIfromlangchain_core.promptsimportChatPromptTemplatefromlangchain_core.output_parsersimportStrOutputParserfromlanggraph.graphimportStateGraph,START,ENDfromconfig_parserimportrouting_config# ---------------------------------------------------------------------------# 状态定义:图的状态,贯穿路由与各处理节点# ---------------------------------------------------------------------------classRoutingState(TypedDict):"""图的状态:请求、路由决策与最终响应。"""request:str# 用户原始请求decision:str# 路由节点输出的分类:booker | info | unclearresponse:str# 当前分支处理节点的输出# ---------------------------------------------------------------------------# LLM 与路由链# ---------------------------------------------------------------------------def_build_llm():"""构建 LLM(从 config 读取 API Key 与 base_url)。"""cfg=routing_config llm=ChatOpenAI(model=cfg.model,api_key=cfg.api_key,base_url=cfg.base_urlifcfg.base_urlelseNone,temperature=0,)returnllm# 路由提示:让 LLM 仅输出一个分类词ROUTER_PROMPT="""分析用户请求,判断应由哪个专门处理器处理。 - 若与预订机票/酒店相关,只输出:booker - 若为一般信息类问题,只输出:info - 若无法归类或不清楚,只输出:unclear 只输出一个词:booker、info 或 unclear。 用户请求: {request}"""def_router_chain():"""路由链:request -> LLM -> decision 字符串。"""prompt=ChatPromptTemplate.from_messages([("user",ROUTER_PROMPT),])llm=_build_llm()returnprompt|llm|StrOutputParser()# ---------------------------------------------------------------------------# 图节点# ---------------------------------------------------------------------------defnode_router(state:RoutingState)->dict:""" 路由节点:调用 LLM 对用户请求做意图分类,写入 state["decision"]。 """chain=_router_chain()raw=chain.invoke({"request":state["request"]})decision=(rawor"").strip().lower()ifdecisionnotin("booker","info","unclear"):decision="unclear"return{"decision":decision}defnode_booking(state:RoutingState)->dict:"""预订处理器:模拟处理机票/酒店预订类请求。"""msg=(f"Booking Handler 已处理请求:'{state['request']}'。""结果:模拟完成预订操作。")return{"response":msg}defnode_info(state:RoutingState)->dict:"""信息处理器:模拟处理一般信息类请求。"""msg=(f"Info Handler 已处理请求:'{state['request']}'。""结果:模拟信息检索与回答。")return{"response":msg}defnode_unclear(state:RoutingState)->dict:"""未识别处理器:无法归类时的兜底回复。"""msg=(f"无法将请求归类:'{state['request']}'。""请补充说明是「预订」还是「一般信息」类问题。")return{"response":msg}# ---------------------------------------------------------------------------# 路由函数:供条件边使用,根据 state["decision"] 返回下一节点名# ---------------------------------------------------------------------------defroute_by_decision(state:RoutingState)->Literal["booking","info","unclear"]:"""根据路由节点的分类结果,返回下一节点名称。"""d=(state.get("decision")or"unclear").strip().lower()ifd=="booker":return"booking"ifd=="info":return"info"return"unclear"# ---------------------------------------------------------------------------# 构建图:START -> router -> [booking | info | unclear] -> END# ---------------------------------------------------------------------------defbuild_routing_graph():"""构建并编译 Routing 图。"""workflow=StateGraph(RoutingState)workflow.add_node("router",node_router)workflow.add_node("booking",node_booking)workflow.add_node("info",node_info)workflow.add_node("unclear",node_unclear)workflow.add_edge(START,"router")workflow.add_conditional_edges("router",route_by_decision,{"booking":"booking","info":"info","unclear":"unclear",},)workflow.add_edge("booking",END)workflow.add_edge("info",END)workflow.add_edge("unclear",END)returnworkflow.compile()

4 解析、校验与节点内重试

4.1 校验失败时的处理策略

按稳妥程度从「就地修复」到「放弃」排序:

  1. 同节点重试:将解析器或校验器返回的validation_errors贴回提示词,要求仅输出修正后的 JSON 段落,不要从头重写。
  2. 降级处理:例如路由解析失败时,默认走向general_faq并记录日志——业务仍可运转,只是体验稍差。
  3. 熔断机制:连续失败时写入 trace 并触发告警(详见第 34、35 篇的可观测性内容)。

4.2 与工具调用的衔接

中间结果尽量只保留业务键(如order_id),不要将整句自然语言塞进工具参数。工具参数侧仍遵循JSON Schema(参考第 2、7 篇),与上一节点的结构化输出字段对齐,避免「模型输出正确,程序却构参错误」的情况。


5 反面教材:为什么「自由 JSON」会拖垮整条链?

  • 字段膨胀:模型喜欢添加notesexplanation等字段。下游若采用宽松模式,脏数据会静默下传;若采用严格模式,每一步都可能整体报错。
  • 过度嵌套:如meta.result.items[0].detail...这类深层路径,人类难以理解,模型也容易写错。扁平、浅层的键名通常更稳定(参见第 3 篇「扁平、稳定键」原则)。
  • 键名语言混用:上一节点用英文键,下一节点用中文键,映射表一多,迟早有人在某次版本更新中遗漏同步。

6 小结

  • 链式任务中,中间输出就是节点间的 API:要像设计小型 REST 接口一样,明确约定schema、枚举值、缺省写法、正反示例
  • 单块输出、字段精简、校验严格、出错就地重试——这比指望「模型一次性输出完美长文档」更可靠。
  • 使用 LangGraph 时,让JSON 键名直接对齐 state,减少「心照不宣」的隐式约定。

如果你在维护多节点图,建议为每个节点准备一份 JSON Schema 文件,并编写三条单元测试:正常情况、缺槽场景、非法枚举——在 CI 中跑通解析和校验,比仅凭肉眼调整提示词更稳定。

http://www.jsqmd.com/news/650309/

相关文章:

  • 完全开源的语言模型学习记录--KeepLora
  • Windows 环境下 mysql 修改数据目录
  • 终极电池保护:BatteryChargeLimit如何让你的手机电池寿命延长一倍
  • 汽配/五金/重机焊接怎么配?细分场景下的点焊机厂家“对号入座”指南 - 深度智识库
  • Midscene.js:AI驱动的跨平台UI自动化终极解决方案
  • B站视频解析工具:轻松获取高质量视频资源的终极指南
  • 如何在phpMyAdmin中解决权限操作卡顿_用户表索引与网络延迟优化
  • 使用HSEM进行核间通信
  • Redis 缓存三大经典问题:穿透击穿雪崩的本质区别与工程实践
  • Hunyuan-MT Pro法律科技实践:合同关键条款高亮+双语对照+风险提示
  • auc代码手撕
  • Ubuntu 20.04 LTS 安装NVIDIA驱动,手把手教你搞定那个烦人的蓝色MOK管理界面
  • Driver Store Explorer完整指南:Windows驱动存储区管理神器
  • 如何配置Navicat试用期重置脚本实现Mac数据库工具无限使用
  • 预算有限必看:COD消解仪高性价比品牌推荐 - 品牌推荐大师
  • Figma设计文件与JSON双向转换的终极解决方案:打破设计与开发的数据壁垒
  • 胡桃讲编程:混音教学第三步|AI 翻唱实操:软件 + 模型 + 索引全安装(全链接无遗漏・老本专属)
  • 天津婚姻纠纷律师 姜春梅:以法为盾以情为桥 守护津门家庭安宁|咨询热线 400-0073-869 - 外贸老黄
  • 从零到一:在vspm1.0原型机上实现除法运算的探索与思考
  • 你的智能硬件还只能‘哔哔’响?试试用ESP32和minimp3做个网络电台或语音提示器
  • 别再让表格撑爆你的LaTeX文档了!tabularx + X列类型保姆级教程
  • 告别迷茫!C#连接三菱PLC的两种方式(逻辑站 vs IP直连)保姆级对比与选择指南
  • K-Means聚类算法完整指南:从原理到实战
  • AI为何不能代替真人写作,毕竟还是仅仅是传递
  • 2026国产企业龙虾工具哪家比较好?推荐这款开源高效智能体平台 - 品牌2025
  • AI为何不能代替真人写作,说教再多毕竟也没有改变现实社会
  • 闲置京东e卡别浪费!3招轻松“盘活”,加入“可可收”更省心 - 可可收
  • 高精度vs高性价比?余氯仪十大品牌选购终极攻略 - 陈工日常
  • 跨越架构鸿沟:ARM平台Kettle ETL部署实战避坑指南
  • 【QGIS实战篇】QGIS 3.40 栅格计算器:从公式到场景的完整工作流