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

张开发
2026/4/16 13:41:20 15 分钟阅读

分享文章

面向 LLM 的程序设计 10:链式任务中的中间输出格式——如何写提示才能稳定得到可解析结构
如果你读过第 6 篇工具调用完整生命周期应该对整条链路已有清晰理解。接下来你会遇到一类新场景模型生成的内容不是给用户阅读而是供下游代码直接消费。打个比方工厂流水线上上一道工序必须按固定规格输出零件下一道才能顺利接手。链式任务中LLM 每个节点的输出就是这道工序的交付物——规格不对整条流水线都要停摆。为什么要专门讨论「中间输出」实际场景中模型常需生成一些中间状态——如路由标签、槽位信息、子任务列表、检索 query、JSON 格式的执行计划等。这些内容不会展示给最终用户却要被json.loads()解析、被条件分支判断、被下游节点读取。中间一步格式出错就像传话游戏传错一个字后面全部乱套。本文是系列第十篇探讨在多步推理、Prompt 链、LangGraph 节点之间如何将中间输出约定为稳定的结构化格式定义字段、类型、缺值处理方式在提示词中给出正反示例并与解析、校验、重试机制配合。核心要点稳定链式结构的关键是「一步一契约、失败就地修复」。每个节点只输出单一封装格式如单层 JSON 或带 XML 标签的 JSON必填字段精简明确缺数据时使用unknown/none等团队约定的哨兵值而非空字符串提示词中提供最小正例 边界反例解析失败时优先在当前节点重试或降级处理这些做法与第 6 篇的工具调用参数构造原则一致单块输出、优先取最后一个合法 JSON、失败时将错误信息回注再生成。关键词Prompt ChainingLangGraph中间表示可解析输出JSON节点契约重试案例代码链接Langgragh 2. 路由 Routing 源代码。0 系列回顾面向 LLM 的程序设计 1API 契约设计从 REST 到「能力端点」。能力化端点为具体业务动作各自暴露的专用接口例如/summarize-document、/list-orders-by-user不要把所有需求都丢进一个万能/ask接口。面向 LLM 的程序设计 2确定性契约为什么 LLM 调用的 API 需要严格 JSON Schema。用 JSON Schema 钉死类型、枚举与必填对冲模型输出的随机性减少歧义与解析失败。面向 LLM 的程序设计 3LLM-Friendly 的响应结构扁平键、稳定字段与类型标注。键名稳定、结构尽量扁平、语义一眼可读方便模型与下游工具链消费。面向 LLM 的程序设计 4API 版本化与演进——在「模型会记忆旧文档」前提下的兼容策略。显式版本、可渐进扩展与废弃公告避免模型仍按旧文档调用已变更接口。面向 LLM 的程序设计 6Tool Calling 的完整生命周期——从定义、决策、执行到观测回注。从工具定义到回注再推理串成闭环每步可校验、可观测、失败可处理。面向 LLM 的程序设计 7工具描述的工程化——name、description、parameters 怎么写才少误用。稳定 name、写清何时用与边界、Schema 与文案一致降低选错工具与填错参的概率。面向 LLM 的程序设计 8「少而宽」还是「多而窄」——工具粒度与 Token 预算的权衡。在工具个数、单工具覆盖面与上下文占用之间做工程权衡平衡误触率与 Token 成本。面向 LLM 的程序设计 9系统提示中的「能力边界」——减少越权与幻觉调用。在系统提示里划清能做与不能做减少越权操作与「假装能调」的幻觉调用。1 什么是「中间输出」简单说不是写给用户读的优美段落而是写给程序读的结构化清单。常见的几种形态路由节点输出intent、topic、next_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 时空字符串与「字段缺失」难以区分——到底是模型判定无此数据还是漏写了与团队约定统一哨兵值使用unknown、none、null之一表达「缺失或不确定」。不要用空字符串或省略字段来糊弄。3 如何写提示词才能避免像布置作文题目标是让模型像填写表格而非像撰写周记。3.1 可复用的小模板每个节点四段式建议每个节点的提示词都包含以下四部分可复制模板后修改名词角色定义你是流水线中的「某某节点」不要与用户寒暄不要输出免责声明占用篇幅。输出规格逐条列出字段名、数据类型、枚举值、是否必填——这就是该节点的「字段清单」。示例提供一个最短正确示例再提供一个信息不足时的示例如槽位填unknown。禁止事项不要输出清单外的字段不要用 Markdown 表格代替 JSON表格对程序不友好。理解要点提示词中多写两行示例往往比事后在日志中排查格式错误、再调整解析器更节省总体成本。3.2 与 LangGraph 的状态字段对齐为什么多一层「JSON 键 → 内存字段」的手工映射迟早有人在某次修改时漏掉一边。如果代码中使用state[router]提示词中 JSON 的顶层键最好直接对应router所需的字段名或采用团队统一命名。提示词中的键 状态机中的键减少隐式约定。结构化 intentslotsqueriesfilters用户输入节点_Router节点_Retrieve节点_Answer3.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 架构状态图 条件边状态StateRoutingState在节点间共享关键中间字段为decisionbooker | info | unclear。路由节点router只负责根据用户请求写入decision不直接调用下游业务节点。路由函数route_by_decision供条件边使用读取state[decision]返回下一节点名booking/info/unclear。条件边从router出发按路由函数返回值映射到三条分支之一。处理节点booking、info、unclear各自生成最终response再连到END。bookerinfounclearSTARTrouterLLM 意图分类写入 decisionroute_by_decision读 state decisionbooking预订类处理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图节点request等response模拟预订机票/酒店类请求的处理结果示例中为占位文案。info图节点request等response模拟一般信息类请求的处理结果。unclear图节点request等response无法归类时的兜底回复引导用户补充意图与路由校验失败时的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_KEY或DASHSCOPE_API_KEY可选BASE_URL、MODEL然后运行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。cfgrouting_config llmChatOpenAI(modelcfg.model,api_keycfg.api_key,base_urlcfg.base_urlifcfg.base_urlelseNone,temperature0,)returnllm# 路由提示让 LLM 仅输出一个分类词ROUTER_PROMPT分析用户请求判断应由哪个专门处理器处理。 - 若与预订机票/酒店相关只输出booker - 若为一般信息类问题只输出info - 若无法归类或不清楚只输出unclear 只输出一个词booker、info 或 unclear。 用户请求 {request}def_router_chain():路由链request - LLM - decision 字符串。promptChatPromptTemplate.from_messages([(user,ROUTER_PROMPT),])llm_build_llm()returnprompt|llm|StrOutputParser()# ---------------------------------------------------------------------------# 图节点# ---------------------------------------------------------------------------defnode_router(state:RoutingState)-dict: 路由节点调用 LLM 对用户请求做意图分类写入 state[decision]。 chain_router_chain()rawchain.invoke({request:state[request]})decision(rawor).strip().lower()ifdecisionnotin(booker,info,unclear):decisionunclearreturn{decision:decision}defnode_booking(state:RoutingState)-dict:预订处理器模拟处理机票/酒店预订类请求。msg(fBooking Handler 已处理请求{state[request]}。结果模拟完成预订操作。)return{response:msg}defnode_info(state:RoutingState)-dict:信息处理器模拟处理一般信息类请求。msg(fInfo 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)orunclear).strip().lower()ifdbooker:returnbookingifdinfo:returninforeturnunclear# ---------------------------------------------------------------------------# 构建图START - router - [booking | info | unclear] - END# ---------------------------------------------------------------------------defbuild_routing_graph():构建并编译 Routing 图。workflowStateGraph(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 校验失败时的处理策略按稳妥程度从「就地修复」到「放弃」排序同节点重试将解析器或校验器返回的validation_errors贴回提示词要求仅输出修正后的 JSON 段落不要从头重写。降级处理例如路由解析失败时默认走向general_faq并记录日志——业务仍可运转只是体验稍差。熔断机制连续失败时写入 trace 并触发告警详见第 34、35 篇的可观测性内容。4.2 与工具调用的衔接中间结果尽量只保留业务键如order_id不要将整句自然语言塞进工具参数。工具参数侧仍遵循JSON Schema参考第 2、7 篇与上一节点的结构化输出字段对齐避免「模型输出正确程序却构参错误」的情况。5 反面教材为什么「自由 JSON」会拖垮整条链字段膨胀模型喜欢添加notes、explanation等字段。下游若采用宽松模式脏数据会静默下传若采用严格模式每一步都可能整体报错。过度嵌套如meta.result.items[0].detail...这类深层路径人类难以理解模型也容易写错。扁平、浅层的键名通常更稳定参见第 3 篇「扁平、稳定键」原则。键名语言混用上一节点用英文键下一节点用中文键映射表一多迟早有人在某次版本更新中遗漏同步。6 小结链式任务中中间输出就是节点间的 API要像设计小型 REST 接口一样明确约定schema、枚举值、缺省写法、正反示例。单块输出、字段精简、校验严格、出错就地重试——这比指望「模型一次性输出完美长文档」更可靠。使用 LangGraph 时让JSON 键名直接对齐 state减少「心照不宣」的隐式约定。如果你在维护多节点图建议为每个节点准备一份 JSON Schema 文件并编写三条单元测试正常情况、缺槽场景、非法枚举——在 CI 中跑通解析和校验比仅凭肉眼调整提示词更稳定。

更多文章