拓冰建站拓冰建站
首页 / 资讯中心 / 正文

用 instructor 两阶段并行生成法:为 gpt-4o 构建一致的复杂故事 DAG

用 instructor 两阶段并行生成法为 gpt-4o 构建一致的复杂故事 DAG【免费下载链接】instructorstructured outputs for llms项目地址: https://gitcode.com/GitHub_Trending/in/instructor大型语言模型LLM在生成包含大量节点的图结构时往往会因为整张图超出模型上下文窗口而产出节点断裂、连接失效的不一致图。本文以选择你自己的冒险Choose Your Own Adventure故事生成为例演示如何利用 instructor 的两阶段方法——先生成故事大纲再并行展开每一个分支——稳定地用 gpt-4o 生成复杂的有向无环图DAG并深入拆解其背后的状态追踪、并行控制与模板渲染原理。读完本文你将掌握一套可复用的结构化输出 并行递归展开模式既能控制生成图的规模与深度又能保证每个分支内部的叙事一致性同时还能把生成时间从串行执行中显著压缩下来。为什么 DAG 很重要DAGDirected Acyclic Graph有向无环图是图论中的一种基础结构每条边都有方向只能单向流动且不存在回路不会绕回之前的节点。这种结构离选择你自己的冒险故事并不遥远——玩家在每个节点有固定的几个选项且故事只能不断向前推进只要把节点理解为一个故事场景、把边理解为玩家的选择一个交互式叙事的分支结构天然就是一个 DAG。对话式 AI 的工作流编排、任务规划、知识图谱构建等领域也都普遍依赖这种结构。挑战让模型一次性生成整棵故事树如果试图让语言模型在单次调用里直接生成整棵故事树很快就会撞上几个限制仅仅在每个节点保留 4 个选项到了第二层就已经有 20 个节点而如果玩家在故事结束前只能做 2 次选择这个故事又显得过于单薄不够有趣。换句话说把整棵树塞进一次提示词几乎必然溢出模型的上下文窗口。模型要么忽略后续节点的细节要么生成出无效、断连的节点。解决思路是改用两阶段方法第一阶段生成一个初始的故事大纲设定、情节、视觉风格等第二阶段基于大纲把各个选择分支并行地逐层展开直到达到预设的最大深度。由于第二阶段的不同分支之间互不依赖它们天然可以并行生成这也是 instructor 的异步客户端与信号量Semaphore在此大显身手的地方。并行故事生成两阶段实现第一阶段生成故事大纲首先用 gpt-4o 生成故事大纲。这一步至关重要大纲提供起始设定、视觉风格以及图片描述用于横幅图后续生成的所有分支都可以引用这份大纲从而尽可能保证不同分支间的一致性。from pydantic import BaseModel from typing import List class GeneratedStory(BaseModel): setting: str plot_summary: str choices: List[str] visual_style: str image_description: str async def generate_story( client: instructor.AsyncInstructor, story_input: RestateStoryInput ): resp await client.create( messages[ { role: user, content: Generate a story with: - Setting: {{ story_input.setting}} - Title: {{ story_input.title }} Rules: - Generate 2-4 initial choices that represent actions - Choices must move story forward - Include brief setting description - Generate a visual description for the story Required Elements: 1. Plot Summary: A vivid description of the setting and plot 2. Initial Choices: 2-4 distinct actions the user can take 3. Visual Style: Description of art style, color palette 4. Image Description: One-sentence scene description , } ], modelgpt-4o, response_modelGeneratedStory, context{story_input: story_input}, ) return resp这段代码有两个值得注意的点response_modelGeneratedStory用 Pydantic 模型约束输出结构确保模型返回的 JSON 一定包含setting、plot_summary、choices、visual_style、image_description五个字段。这正是 instructor 的核心能力——把 LLM 的自由文本输出转换为类型安全的结构化对象。context{story_input: story_input}提示词里的{{ story_input.setting }}、{{ story_input.title }}是 Jinja 模板变量由context参数注入。在 instructor/v2/core/templating.py 中可以看到instructor 使用jinja2.sandbox.SandboxedEnvironment渲染消息内容def apply_template(text: str, context: dict[str, Any]) - str: Apply Jinja2 template to the given text. return dedent(SandboxedEnvironment().from_string(text).render(**context))调用完成后会得到一个类似如下的结构化结果# Example generated output { setting: A neon-lit cyberpunk metropolis in 2150, plot_summary: In the sprawling city of Neo-Tokyo..., choices: [ Investigate the mysterious signal in the abandoned district, Meet your contact at the underground hacker hub, Follow the corporate executive who seems suspicious ], visual_style: Vibrant neon colors, detailed cyberpunk architecture, image_description: A towering cyberpunk cityscape at night with neon signs }第二阶段并行展开分支生成深层故事树时最大的难点是随着分支不断生长如何维持一致性。这里的解法是并行生成 状态追踪关键洞察是故事树中的每一条路径都拥有自己独立的状态。实现方式是维护一个简单的累加器accumulator持续记录此前做过的选择以及故事上下文。同时模型在任何节点都保有随时结束故事的完整自由度。async def rewrite_choice( client: instructor.AsyncInstructor, choice: str, story: GeneratedStory, prev_choices: list[dict], # Accumulator for path state max_depth: int, sem: asyncio.Semaphore, ) - FinalStoryChoice: # Each choice knows its entire path history async with sem: rewritten_choice await client.create( modelgpt-4o, response_modelRewrittenChoice, messages[ { role: user, content: Given this choice: {{ choice }} Story context: Setting: {{ story.setting }} Plot: {{ story.plot_summary }} Previous choices made in this path: {% for prev in prev_choices %} - {{ prev.choice_description }} Result: {{ prev.choice_consequences }} {% endfor %} Generate the next story beat and 2-4 new choices. The story should end in {{ max_depth - len(prev_choices) }} more turns. , } ], context{ choice: choice, story: story, prev_choices: prev_choices, }, ) # For terminal nodes (at max depth) if len(prev_choices) max_depth - 1: return FinalStoryChoice( choice_descriptionrewritten_choice.choice_description, choice_consequencesrewritten_choice.choice_consequences, choices[], # Terminal node ) # Recursively expand child choices child_choices await asyncio.gather( *[ rewrite_choice( clientclient, choicenew_choice, storystory, prev_choicesprev_choices [ { choice_description: rewritten_choice.choice_description, choice_consequences: rewritten_choice.choice_consequences, } ], max_depthmax_depth, semsem, ) for new_choice in rewritten_choice.choices ] ) return FinalStoryChoice( choice_descriptionrewritten_choice.choice_description, choice_consequencesrewritten_choice.choice_consequences, choiceschild_choices, )这套递归实现有几个值得深入理解的设计累加器传值而非共享每个子节点收到的是prev_choices [当前选择]——这是 Python 中构造新列表的传值写法不会污染兄弟分支的状态。因此不同分支可以放心并发执行各自持有完整的路径历史。模板里的循环与算术{% for prev in prev_choices %}逐条列出路径历史{{ max_depth - len(prev_choices) }}计算剩余轮数。这些都是 instructor 模板引擎原生支持的 Jinja 能力详见 docs/concepts/templating.md 中关于 Jinja 语法的介绍。asyncio.gather负责并行一个节点下的所有子节点同时发起请求而sem信号量则把并发的 API 请求控制在可控范围内。两者结合既榨干了并行度又不会打爆上游的速率限制。这种方法带来的收益非常明确路径特定上下文Path-Specific Context每个节点都维护到达它的完整选择历史保证分支内部叙事自洽并行生成Parallel Generation各分支持有独立状态可以同时生成可控增长Controlled Growthmax_depth参数阻止了指数级爆炸限流Rate Limiting信号量控制并发调用数量同时允许最大限度的并行化。信号量的作用不只是限流——它让系统在处理分支时维持一个可控的节奏同时保持状态一致性。故事树中的每一条路径都变成了一部自带完整历史的独立叙事因而能够在远比单次调用更快的速度与更大的篇幅下生成连贯的故事。与此同时生成的故事在广度和深度上也远超单次调用所能达到的规模。从源码看 instructor 是如何支撑这个模式的这个两阶段模式能顺畅运行离不开 instructor 在三个层面的基础设施支撑均可在当前仓库源码中得到印证。1. 异步客户端与模板渲染generate_story与rewrite_choice中使用的instructor.AsyncInstructor定义在 instructor/v2/core/client.py其create方法的签名中包含了context: dict[str, Any] | None None参数同一文件 client.py。这个context会一路传递到模板渲染层在 instructor/v2/core/templating.py 的handle_templating中系统会根据不同的 providerOpenAI、Anthropic、Cohere、Gemini、VertexAI分发到对应的消息处理函数所有模板统一用jinja2.sandbox.SandboxedEnvironment渲染这意味着沙箱环境禁止在提示词中执行任意 Python 代码但列表、条件、算术等常用 Jinja 语法如本文示例中的{% for %}与max_depth - len(...)均可用。2. 结构化输出的类型安全response_modelGeneratedStory、response_modelRewrittenChoice之所以能可靠地拿到字段齐全的对象是因为 instructor 会把 Pydantic 模型转换成 OpenAI Function Calling 的工具 schema并校验模型返回的结果。仓库中 instructor/v2/dsl/parallel.py 的ParallelBase就是这一模式的典型体现——它把多个模型注册进 registry再从 tool_calls 中按函数名取出参数并调用model_validate_json解析parallel.py。3. 并行工具的另一种玩法本文用的是业务层手动并行asyncio.gather而 instructor 还提供了一层原生的并行抽象Mode.PARALLEL_TOOLSresponse_modelIterable[ModelA | ModelB]让模型在单次请求中通过多次工具调用返回多个不同类型对象。详见 docs/concepts/parallel.md。两者的取舍是故事树这种同一节点递归展开的场景适合本文的手动并行模式而一条消息里同时抽多个互不相关的实体则适合后者。超越故事生成这套模式的通用价值这个方案的成功可以归结为三条原则状态隔离State Isolation每个节点只维护它需要的上下文避免上下文窗口溢出并行处理Parallel Processing不同分支可同时生成显著缩短总生成时间结构化验证Structured Validation用 Pydantic 模型保证每个生成组件符合需求。举个直观的量化例子生成一棵 20 节点的故事树如果串行执行、每个节点耗时 3 秒总共约需 60 秒而采用并行生成 10 个并发请求可能只需 4550 秒即可完成。这个模式在以下场景中尤其有价值生成任务天然构成树状或图状结构单个节点只需要祖先上下文的一部分而非全部需要生成超出单个上下文窗口的内容对生成速度有较高要求。另外如果你的提示词中需要携带敏感的用户信息例如在context中传入地址、手机号建议像 docs/concepts/templating.md 中Working with Secrets一节演示的那样用 Pydantic 的SecretStr类型包装敏感字段避免敏感信息被硬编码进提示词或被日志捕获同时由于模板引擎使用沙箱环境仍然建议对所有传入模板的输入做消毒处理以防拒绝服务类攻击。小结当内容量超出单次上下文窗口、且内容天然具有树状/图状结构时单次生成必然失败而两阶段并行展开是一套可靠且可推广的解法先用一次结构化调用锁定全局大纲保证基调一致再让每条路径在完整路径历史的陪伴下递归、并行地长出子分支保证局部叙事连贯。instructor 在其中提供了模板渲染context Jinja、结构化校验Pydanticresponse_model与异步客户端AsyncInstructor三块基石让生成复杂、互联的内容变得像写一个递归函数一样简单可控。instructor让用语言模型生成复杂数据结构变得简单——无论是 ollama 上的开源模型还是 OpenAI 等专有模型都可以套用这一模式。仓库内的 examples 目录还提供了大量可运行的参考实现可以作为继续深入的第一手素材。【免费下载链接】instructorstructured outputs for llms项目地址: https://gitcode.com/GitHub_Trending/in/instructor创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门