用 Outlines 从一段文字描述生成结构化的约会应用资料(Pydantic + JSON Schema 实战)
【免费下载链接】outlinesStructured Outputs项目地址: https://gitcode.com/GitHub_Trending/ou/outlines
本文以 Outlines 仓库中的官方示例 dating_profiles.md 为主体,演示如何用Pydantic 模型定义输出结构、用Jinja 模板批量渲染 Few-shot 提示词,并通过结构化生成(constrained generation)让 LLM 直接从一句个人描述产出完全符合字段约束(bio 长度、interests 数量、预设 Q&A 选项)的 JSON 数据。读完本文,你将掌握Template、outlines.from_transformers、json_schema/Pydantic 输出类型与Generator的完整配合方式,并具备把这一套「提示词工程 + 结构化解码」流水线复用到其他合成数据生成场景的能力。
该示例最早由社区贡献者 Vibhor Kumar 提供,仓库中对应的可运行脚本位于 examples/dating_profile.py,提示词模板位于 examples/prompts/dating_profile.txt,本文将以文档为骨架,结合源码逐层讲解。
示例的目标:用一句描述合成一个约会资料
一个典型的约会 App 资料包含:一段自我介绍(bio)、职业(job)、一组兴趣(interests)以及两个「预设问题-答案」对(Q&A)。这类数据通常需要人工撰写,成本高且难以规模化。本示例的思路是:
- 用 Pydantic 把「资料」定义为强类型的数据结构,并声明字段级约束;
- 用 Outlines 的
Template编写带 Few-shot 示例的提示词,让模型扮演「红娘」; - 用
outlines把模型封装为可结构化生成的实例,直接传入 Pydantic 类作为输出类型; - 模型输出的每一步解码都被 logits processor 约束,保证最终产物一定是一个合法 JSON、且字段一定满足声明的约束,随后用
DatingProfile.model_validate_json完成校验与解析。
整个过程无需任何后处理修复、重试或正则兜底,这正是结构化生成相比「纯提示词」的核心优势。
第一步:用 Pydantic 定义资料结构
文档中,资料结构由三层类型组合而成:一个枚举QuestionChoice(预设问题选项)、一个 dataclassQuestionAnswer(问题-答案对),以及一个 PydanticBaseModel子类DatingProfile(完整资料)。
import json from dataclasses import dataclass from enum import Enum import torch import transformers from pydantic import BaseModel, conlist, constr import outlines预设问题选项:继承 str 的 Enum
class QuestionChoice(str, Enum): A = "The key to my heart is" B = "The first item on my bucket list is" C = "Perks of dating me" D = "Message me if you also love" E = "People would describe me as" F = "I can beat you in a game of" @dataclass class QuestionAnswer: question: QuestionChoice answer: str这里的关键细节是class QuestionChoice(str, Enum):枚举成员同时是str,因此在 JSON 序列化时输出的是字符串值(如"The first item on my bucket list is")而非整数索引,且答案字段可以任意填写。
资料模型:带长度与数量约束
class DatingProfile(BaseModel): bio: constr(str, min_length=10, max_length=300) job: constr(str, max_lengt=50) interests: conlist(str, min_length=1, max_length=5) # type: ignore qna1: QuestionAnswer qna2: QuestionAnswerbio:必填,长度限制在10~300 个字符,防止模型输出过短或失控的自我介绍;job:必填,最多50 个字符;interests:元素为str的列表,数量限制在1~5 个,避免兴趣列表过长;qna1/qna2:两个嵌套的QuestionAnswer对象。
注意:文档示例中的
max_lengt是笔误,实际应为max_length。另外constr/conlist属于 Pydantic v1 风格 API(因此加了# type: ignore规避 mypy 对旧式约束类型支持不佳的问题,见 examples/dating_profile.py 中的注释)。仓库中实际运行版本 examples/dating_profile.py 已将bio/job简化为普通str——脚本注释说明:对字符串使用constr长度约束会显著增加生成时间(当时跟踪于 dottxt-ai/outlines 的 PR #272 讨论)。如果你在较新的 Pydantic v2 上复现,可将长度校验交给生成后的model_validate_json环节,或使用 v2 的Field(min_length=..., max_length=...)。
第二步:用 Outlines 的 Template 编写提示词
提示词模板:模板逻辑与示例数据分离
文档强调使用 Outlines 的模板能力来生成提示词,这样可以把「通用的提示逻辑」与「每个示例特有的数据」清晰分离:
from outlines import Template dating_profile_prompt = Template.from_string( """ You are a world-renowned matchmaker who understands the modern dating market. Your job is to generate dating app profiles for male clients interested in women based on a provided description. The profiles should be authentic, show off their strengths, and maximize their likelihood of getting matches on dating apps. Here are some examples of past clients that you have successfully created profiles for: {% for example in examples %} Description: {{ example.description }} Profile: {{ example.profile }} {% endfor %} Here is the new client who you need to create a profile for: Description: {{ description }} Profile: """ )模板基于 Jinja2,{% for example in examples %}循环渲染每个 Few-shot 示例,{{ ... }}输出字段值。仓库中 examples/prompts/dating_profile.txt 保存了同样的模板内容,可改用Template.from_file("prompts/dating_profile.txt")从文件加载(参见 examples/dating_profile.py)。
源码视角:Template 的实现细节
从源码看,src/outlines/templates.py 中的Template是一个 dataclass,核心逻辑如下:
Template.from_string(content, filters={}):把字符串模板交给build_template_from_string编译。该函数先用inspect.cleandoc去除缩进、用正则压缩多余空白,再放进 Jinja2 环境(src/outlines/templates.py)。Template.from_file(path, filters={}):以模板文件所在目录为基准创建FileSystemLoader,支持 Jinja2 的 include 与模板继承,但不能引用文件所在目录之外的资源(src/outlines/templates.py)。- Jinja2 环境预设了 6 个过滤器:
name(函数名)、description(docstring 首行)、source(函数源码)、signature(函数签名)、schema(Pydantic 模型/字典的 JSON Schema)、args(函数参数),且允许用户传入filters覆盖内置过滤器(src/outlines/templates.py)。 - 环境使用
StrictUndefined与trim_blocks=True、lstrip_blocks=True:模板中任何未定义的变量都会直接抛错,从根上避免「变量名拼写错误导致提示词静默缺失」这类难以排查的问题。 Template实例可直接调用:dating_profile_prompt(description=..., examples=...)等价于template.render(**kwargs),返回渲染后的提示词字符串(src/outlines/templates.py)。
你还可以在模板中使用{{ example.profile | schema }}之类的过滤器,把 Pydantic 模型序列化为紧凑的 JSON Schema 片段作为提示词的一部分(get_schema对 Pydantic 模型会自动递归解析$defs/definitions引用,见 src/outlines/templates.py),适合需要在提示词中显式暴露输出结构的场景。
第三步:提供 Few-shot 示例
接下来为模型提供三个高信息量的 Few-shot 示例,覆盖不同的职业、生活方式与语气风格:
samples: list[Example] = [ Example( description="I'm an author and former professional soccer player living in Seattle who publishes popular fiction books. A typical day for me starts by hanging out with my cat, drinking a coffee, and reading as much as I can in a few hours. Then, I'll prepare a quick smoothie before starting to write for a few hours, take a break with soccer or running a few miles, and finally meet friends for dinner at a new, hip restaurant in the evening. Sometimes we go axe-throwing afterwards, or play poker, or watch a comedy show, or visit a dive bar. On my vacations, I travel extensively to countries South America, Europe, and Asia, with the goal of visiting them all!", profile=DatingProfile( bio="Adventurer, dreamer, author, and soccer enthusiast. Life’s too short to waste time so I make the most of each day by exploring new places and playing with my friends on the pitch. What’s your favorite way to get out and have fun?", job="Famous Soccer Player -> Famous Author", interests=["Soccer", "Travel", "Friends", "Books", "Fluffy Animals"], qna1=QuestionAnswer( question=QuestionChoice.B, answer="swim in all seven oceans!" ), qna2=QuestionAnswer( question=QuestionChoice.E, answer="fun-loving, adventurous, and a little bit crazy", ), ), ), Example( description="I run my company and build houses for a living. I'm a big fan of the outdoors and love to go hiking, camping, and fishing. I don't like video games, but do like to watch movies. My love language is home-cooked food, and I'm looking for someone who isn't afraid to get their hands dirty.", profile=DatingProfile( bio="If you're looking for a Montana man who loves to get outdoors and hunt, and who's in-tune with his masculinity then I'm your guy!", job="House Construction Manager / Entrepreneur", interests=["Hunting", "Hiking", "The outdoors", "Home-cooked food"], qna1=QuestionAnswer(question=QuestionChoice.A, answer="food made at home"), qna2=QuestionAnswer( question=QuestionChoice.C, answer="having a man in your life who can fix anything", ), ), ), Example( description="I run my own Youtube channel with 10M subscribers. I love working with kids, and my audience skews pretty young too. In my free time, I play Fortnite and Roblox. I'm looking for someone who is also a gamer and likes to have fun. I'm learning Japanese in my free time as well as how to cook.", profile=DatingProfile( bio="Easy on the eyes (find me on Youtube!) and great with kids. What more do you need?", job="Youtuber 10M+ subscribers", interests=["Kids", "Gaming", "Japanese"], qna1=QuestionAnswer(question=QuestionChoice.D, answer="anime and gaming!"), qna2=QuestionAnswer(question=QuestionChoice.F, answer="Fortnite, gg ez"), ), ), ]@dataclass class Example: description: str profile: DatingProfile每个Example都由「一段自然语言的自述」和「一份符合DatingProfile结构的资料」组成。注意interests均控制在 1~5 个,Q&A 的问题均来自QuestionChoice枚举——这既给模型提供了风格的参照,也让模型从示例中学习「描述 → 结构化资料」的映射模式。
第四步:加载模型并用 Outlines 封装
文档选用的模型是 MosaicML 的MPT-7B-8K-Instruct(mosaicml/mpt-7b-8k-instruct),该模型约需13GB GPU 显存,可以放进单张 GPU 并支持较大的上下文窗口(8K),足以容纳三个 Few-shot 示例加一段新客户描述:
MODEL_NAME = "mosaicml/mpt-7b-8k-instruct" config = transformers.AutoConfig.from_pretrained( MODEL_NAME, trust_remote_code=True ) config.init_device = "meta" model_kwargs = { "config": config, "trust_remote_code": True, "torch_dtype": torch.bfloat16, "device_map": "cuda", } tf_model = transformers.AutoModelForCausalLM.from_pretrained(MODEL_NAME, **model_kwargs) tf_tokenizer = transformers.AutoTokenizer.from_pretrained(MODEL_NAME) model = outlines.from_transformers(tf_model, tokenizer=tf_tokenizer)要点说明:
config.init_device = "meta"+device_map = "cuda":先在 meta 设备上构建模型骨架,再由 transformers 按层分配到 GPU,以节省初始化时的显存峰值;torch_dtype = torch.bfloat16:以 bf16 半精度加载,进一步降低显存占用;trust_remote_code = True:MPT 系列依赖自定义建模代码,必须显式开启;outlines.from_transformers(tf_model, tokenizer=tf_tokenizer):把 transformers 的模型与 tokenizer 包装成 Outlines 的Transformers模型实例。
源码视角:from_transformers 做了什么
从 src/outlines/models/transformers.py 看,from_transformers(model, tokenizer_or_processor, *, device_dtype=None)接收PreTrainedModel与PreTrainedTokenizer/ProcessorMixin,返回Transformers或TransformersMultiModal实例。其内部会:
- 用
TransformerTokenizer包装 tokenizer:若pad_token_id缺失则自动回退为eos_token_id(src/outlines/models/transformers.py),这对批量解码与结构化生成时的填充至关重要; - 处理 chat template 检查(
_check_hf_chat_template)与输入适配(TransformersTypeAdapter支持str与Chat两类输入,见 src/outlines/models/transformers.py); - 为后续的结构化生成提供词表与 logits 处理所需的模型接口。
Transformers属于「可操纵模型」(SteerableModel),因此它支持 logits-level 的约束解码;如果你使用 OpenAI 等远程 API 模型,则属于黑盒模型,输出类型会被直接传给服务端而不是编译成本地 logits processor(详见 src/outlines/generator.py 中BlackBoxGenerator与SteerableGenerator的区分)。
第五步:结构化生成一份新资料
准备好新客户的描述后,渲染提示词并直接以DatingProfile作为输出类型调用模型:
new_description = """I'm a laid-back lawyer who spends a lot of his free-time gaming. I work in a corporate office, but ended up here after the start-up I cofounded got acquired, so still play ping pong with my cool coworkers every day. I have a bar at home where I make cocktails, which is great for entertaining friends. I secretly like to wear suits and get a new one tailored every few months. I also like weddings because I get to wear those suits, and it's a good excuse for a date. I watch the latest series because I'm paying, with my hard-earned money, for every streaming service.""" prompt = dating_profile_prompt(description=new_description, examples=samples) profile = model(prompt, DatingProfile) parsed_profile = DatingProfile.model_validate_json(json.loads(profile))# 仓库脚本中的等价写法(examples/dating_profile.py) profile = model(prompt, outlines.json_schema(DatingProfile), max_tokens=500)两种写法都可以:直接传 Pydantic 类DatingProfile,或显式用outlines.json_schema(DatingProfile)包装。model(prompt, DatingProfile)返回的profile是一个保证符合 JSON Schema 的字符串,因此可以放心地交给DatingProfile.model_validate_json(json.loads(profile))做最终的类型校验与对象化——model_validate_json是 Pydantic v2 的原生方法,会再次核对 bio 长度、interests 数量、枚举取值等约束。
源码视角:输出类型如何变成解码约束
当以 Pydantic 类作为输出类型调用模型时,底层实际走的是Generator(见 src/outlines/generator.py):
Generator(model, output_type)会把 output_type 交给python_types_to_terms归一化为 Outlines 内部的 Term 对象(CFG/JsonSchema/Regex);- 若是
JsonSchema,则调用get_json_schema_logits_processor(backend_name, model, term.schema, term.whitespace_pattern)编译出一个JSON Schema logits processor;若是 Pydantic 类,其model_json_schema()会被用来推导 JSON Schema(json_schema工厂函数定义于 src/outlines/types/dsl.py); - 每次调用
model(prompt)时,SteerableGenerator.__call__会先reset()该 logits processor 的状态,再传给模型的generate(src/outlines/generator.py)。
换句话说:在每一个解码步,模型只能从「当前已生成前缀仍可能构成合法 JSON 与合法约束值」的 token 集合中选择下一个 token,因此输出在构造过程中就必然满足 schema,而不是靠事后修补。logits processor 的构建较为昂贵,Generator会把它缓存并复用,这也是示例可以反复调用而无需重复编译的原因。更完整的生成器语义可参考 docs/features/core/generator.md。
运行结果
文档给出了两份由模型生成的真实输出,全部符合DatingProfile的结构(bio 在 10~300 字符内、interests 为 5 个、Q&A 问题均来自枚举):
{ "bio": """I'm an ambitious lawyer with a casual and fashionable style. I love games and sports, but my true passion is preparing refreshing cocktails at home and dressing to the nines at weddings. I'm currently looking for a woman to show a good time to and get a kiss on the opulent suit I just had made. Send resume to this inbox.""", "job": "Lawyer", "interests": [ "Stylish guys", "Gaming", "Ping pong", "Cocktails", "Weddings" ], "qna1": { "question": "The first item on my bucket list is", "answer": "be married and have a family." }, "qna2": { "question": "People would describe me as", "answer": "charming, stylish, and funny." } }{ "bio": """I’m a sexy lawyer with time on my hands. I love to game and play ping pong, but the real reason you should swipe to the right is because I look great in a suit. Who doesn’t love a man in a suit? Just saying. Send me a message if you think it’s time to take your dating life to the next level.""", "job": "Lawyer", "interests": [ "Gaming", "Ping Pong", "Tailored Suits", "Weddings", "Streaming Services" ], "qna1": { "question": "The first item on my bucket list is", "answer": "simulate space but stay alive for as long as possible" }, "qna2": { "question": "People would describe me as", "answer": "easy-going, a little nerdy but with a mature essence" } }可以看到:模型从描述中准确提炼出了「律师」「游戏」「乒乓球」「鸡尾酒」「西装」「婚礼」「流媒体订阅」等关键要素,并以风格各异的文案完成了两份资料,同时所有字段都严格满足声明约束。job收敛为简洁的"Lawyer",interests恰好 5 项,Q&A 的问题文本与枚举完全一致。
在仓库中复现与继续探索
- 完整可运行脚本:examples/dating_profile.py(其中
Template.from_file("prompts/dating_profile.txt")从 examples/prompts/dating_profile.txt 加载同一份模板); - 本文对应的文档原文:docs/examples/dating_profiles.md;
- 更多示例:同一目录下还有 extract_event_details.md(事件信息抽取)、chain_of_density.md(摘要密度链)、knowledge_graph_extraction.md(知识图谱抽取)等,均复用「Pydantic 定义结构 + 模板渲染 Few-shot + 结构化生成」的同一套路;
- 模板系统的完整能力(内置过滤器、自定义过滤器、
schema过滤器)见 src/outlines/templates.py 与 template.md; - 输出类型体系(
json_schema、regex、cfg及内置的date、uuid4、email等便捷类型)见 src/outlines/types/init.py 与 types 文档; - 如果你希望换用更易获取的模型(如 Llama/Qwen 系列或远程 API),可参考 models/transformers.md、models/openai.md 与 selecting_an_inference_backend.md;环境安装见 installation.md。
小结:可复用的「合成数据生成」流水线
这个例子虽然主题是约会资料,但它的工程骨架完全可以迁移到任何「给定自然语言描述、产出结构化数据」的场景(简历生成、商品文案、客服话术、问卷作答、知识库条目……):
- 用 Pydantic 精确刻画目标数据结构(嵌套模型、枚举、长度/数量约束);
- 用
Template分离提示逻辑与示例数据,把高质量 Few-shot 样本组织成模板循环; - 用
outlines.from_transformers封装本地模型,或换用任意受支持的后端; - 把 Pydantic 类直接作为输出类型调用,让 logits processor 在解码期保证 JSON 合法性与字段约束;
- 用
model_validate_json收尾校验,得到类型安全的 Python 对象。
相比纯提示词方案,这套流水线把「格式正确性」从概率问题变成了确定性保证——这正是 Outlines 作为结构化生成框架的价值所在。
【免费下载链接】outlinesStructured Outputs项目地址: https://gitcode.com/GitHub_Trending/ou/outlines
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考