用 Outlines 实现 SimToM 两阶段视角代入 Agent:从 Prompt 模板到结构化生成的完整实践
2026/9/14 7:26:42 网站建设 项目流程

用 Outlines 实现 SimToM 两阶段视角代入 Agent:从 Prompt 模板到结构化生成的完整实践

【免费下载链接】outlinesStructured Outputs项目地址: https://gitcode.com/GitHub_Trending/ou/outlines

本篇技术指南以 Outlines 的官方示例文档 docs/examples/simtom.md 为主体,讲解如何在当前仓库中用 Outlines 的 Prompt 模板(Jinja2)与结构化生成能力,以十几行代码复现 SimToM(Simulation Theory of Mind)两阶段提示框架:先让 LLM 站在指定角色视角过滤故事信息,再基于该视角回答需要"心智理论"(Theory of Mind)推理的问题。读完本文,你将掌握Template.from_file加载 Jinja2 提示词模板、用 Pydantic 模型约束 JSON 结构化输出、通过outlines.from_transformers接入 Hugging Face transformers 模型并执行两轮受约束生成,从而构建一个可应用于 Agentic 工作流中的"视角代入型"推理链路。

SimToM:面向不一致世界状态的两阶段提示框架

在需要跟踪多个角色各自掌握的不同信息(即"不一致的世界状态")的任务中,常见的 Chain-of-Thought(CoT)等提示策略往往力不从心——模型默认以"全知视角"作答,容易把角色不该知道的信息混入推理。SimToM 受 Simulation Theory(模拟理论)启发,提出一个简洁的两阶段提示框架,其核心思路是显式地把"推理"与"信息获取"解耦:

  1. 视角代入(Perspective-Taking):第一轮提示接收一段story(事件序列)和指定的character,目标是让模型以该角色的视角理解情境,只保留"这个角色知道的事件",过滤掉其余内容;
  2. 问答(Question-Answering):第二轮提示接收上一步得到的"角色视角事件列表",要求模型仅基于该上下文回答给定question

根据论文报告(SimToM 原文,arXiv:2311.10227),该框架在 ToMI 与 BigToM 这两个包含 Theory of Mind 问题的基准上优于 zero-shot 提示与 CoT。它的价值不仅在于评测场景,还在于 Agent 系统:Agent 应当基于"自己所知道的信息"行动,而非利用全局可见的全部信息——SimToM 恰好把这种知识边界显式建模出来。

视角代入的三条规则

仓库中保留了两份与本文示例配套的真实提示词模板,其中视角代入模板 docs/examples/prompt_templates/simtom_prospective_taking.txt 明确列出了判断"角色知道什么"的三条规则,这是 SimToM 在第一阶段过滤事件的核心依据:

  1. 角色知道自己参与的所有事件
  2. 角色身处某地点时,知道该地点发生的所有其他事件(包括他人进出、物体位置、物体被移动等);
  3. 角色离开某地点后,不再知道该地点内发生的事件(但可以再次进入)。

Outlines 实现总览:三步完成 SimToM

在 Outlines 中实现 SimToM 只需三个步骤,全部围绕仓库核心 API 展开:

  1. Prompt 模板outlines.Template)定义两轮提示词;
  2. Pydantic 模型声明每一轮要返回的 JSON 结构;
  3. transformers 集成outlines.from_transformers)加载本地 LLM 并生成受约束的结构化响应。

第一步:使用 Prompt 模板装载提示词

作者公开了 SimToM 的代码、提示词与数据。当前仓库已把 ToMI 数据集使用的两份提示词落盘为模板文件,我们可以直接通过Template.from_file加载:

from outlines import Template perspective_taking = Template.from_file("prompt_templates/simtom_prospective_taking.txt") simulation = Template.from_file("prompt_templates/simtom_simulation.txt")

注意:上述路径为示例文档中的写法。在当前仓库中,这两份模板的实际位置是 docs/examples/prompt_templates/simtom_prospective_taking.txt 与 docs/examples/prompt_templates/simtom_simulation.txt,请按实际仓库路径加载。

从源码看,Template是 src/outlines/templates.py 中定义的一个 dataclass,其内部封装了一个jinja2.Template实例,并且可调用template(**kwargs)会调用self.template.render(**kwargs)返回渲染后的字符串。from_file通过jinja2.FileSystemLoader以模板文件所在目录为根来构建 Jinja2 环境,因此支持 include/继承等 Jinja2 特性,但不允许引用模板文件目录之外的相对文件from_string则从字符串构建模板,并会调用inspect.cleandoc做去缩进处理。

Jinja2 环境在 create_jinja_env 中预置了若干实用 filter:name(函数名)、description(函数 docstring 首行)、source(函数源码)、signature(函数签名)、schema(Pydantic 模型 JSON Schema)、args(函数参数),也允许用户传入自定义 filter 覆盖内置项。环境启用了trim_blockslstrip_blockskeep_trailing_newline,并将未定义变量设为StrictUndefined——任何模板变量的拼写错误都会在渲染阶段立刻报错,而非静默输出空串。

第二轮问答模板 docs/examples/prompt_templates/simtom_simulation.txt 充分利用了 Jinja2 的循环语法,把事件列表逐条渲染进提示词:

<s>[INST] {% for event in events %} {{event}} {% endfor %} You are {{name}}. Based on the above information, answer the following question: {{question}} You must choose one of the above choices, do not say there is not enough information. Answer with a single word, do not output anything else. [/INST]

第二步:用 Pydantic 定义 JSON 结构化输出

Outlines 会保证 LLM 返回合法的 JSON 对象,且其结构由你指定的 Pydantic 模型决定(在Generator中,Pydantic 模型会先被转换为术语表示,再编译为对应后端的 logits 处理器,详见 src/outlines/generator.py)。SimToM 需要两个 Pydantic 模型,分别对应两轮提示:

from pydantic import BaseModel, Field from typing import List class PerspectiveTaking(BaseModel): """This is for the first prompt.""" character: str = Field(description="The character we extract the events for.") events: List[str] = Field(description="All events that the character knows about.") class Simulation(BaseModel): """This is for the second prompt.""" answer: str
  • PerspectiveTaking承载第一轮输出:character字段记录被考察的角色,events字段以字符串列表保存该角色所知的事件;
  • Simulation承载第二轮输出:answer字段记录最终单字答案。

Field(description=...)中的描述信息会进入 Pydantic 生成的 JSON Schema,模板环境中内置的schemafilter 在渲染 Pydantic 模型的 schema 时也会优先取用这些描述(见 src/outlines/templates.py),让模型在生成时明确每个字段的语义。

第三步:调用 LLM 完成两轮受约束生成

以 ToMI 数据集中的一则小故事为例。故事由 6 条按编号排列的事件组成:Aria 与 Aiden 先后进入院子、西柚被放在绿桶中、Aria 把西柚移到蓝色容器、Aiden 离开院子、Noah 进入游戏室。问题问的是"西柚最初在哪里",角色指定为 Aria——而 Aria 在事件 4 之后才操作西柚,且事件 3 发生时她在场,所以她知道西柚原本在绿桶里,却不知道事件 2(Aiden 进入院子)这类与她无关的信息:

story = """ 1 Aria entered the front_yard. 2 Aiden entered the front_yard. 3 The grapefruit is in the green_bucket. 4 Aria moved the grapefruit to the blue_container. 5 Aiden exited the front_yard. 6 Noah entered the playroom. """ question = "7 Where was the grapefruit at the beginning?" character = "Aria"

接着用outlines.from_transformers加载 Hugging Face 上的Mistral-7B-Instruct-v0.3。从 src/outlines/models/transformers.py 的源码可以看到,from_transformers接受一个PreTrainedModel与一个 tokenizer(或ProcessorMixin,此时返回多模态模型),返回Transformers实例;Transformers内部会通过TransformersTypeAdapter在检测到模型带 chat template 时自动应用apply_chat_template(见 src/outlines/models/transformers.py),并统一设置 tokenizer 的padding_side = "left"

第一轮:渲染视角代入提示词,生成PerspectiveTaking结构:

import transformers import outlines # Load an LLM from Hugging Face MODEL_NAME = "mistral-community/Mistral-7B-Instruct-v0.3" model = outlines.from_transformers( transformers.AutoModelForCausalLM.from_pretrained(MODEL_NAME), transformers.AutoTokenizer.from_pretrained(MODEL_NAME), ) perspective_prompt = perspective_taking(story=story, character=character) # Call Mistral 7B with the first prompt generator = outlines.Generator(model, PerspectiveTaking) perspective = generator(perspective_prompt, max_new_tokens=1024) print(perspective) # {'character': 'Aria', 'events': ['1 Aria entered the front_yard.', '3 The grapefruit is in the green_bucket.', '4 Aria moved the grapefruit to the blue_container.']}

outlines.Generator(见 src/outlines/generator.py)把"模型 + 输出类型"封装为可复用对象:构造时根据输出类型编译并缓存 logits 处理器(构造开销较大,缓存后多次调用不再重复编译),每次__call__前会调用logits_processor.reset()再传入模型的generate。输出中事件 1、3、4 被正确保留,而事件 2、5、6(Aria 不知道的其他角色行为)被过滤——这正是第一轮的目标。

第二轮:把第一轮得到的events注入模拟模板,基于 Aria 的视角回答问题:

import json sim_prompt = simulation(events=json.loads(perspective)["events"], name=character, question=question) # Call Mistral 7B with the second prompt generator = outlines.Generator(model, Simulation) result = generator(sim_prompt, max_new_tokens=1024) print(result) # {'answer': 'green_bucket'}

注意outlines.Generator(model, PerspectiveTaking)返回的是序列化后的 JSON 字符串,因此在注入第二份模板前需要先用json.loads还原成 Python 对象再取events字段。最终模型基于 Aria 视角得出green_bucket——因为事件 4 的"移动西柚"发生在 Aria 进入院子之后,她清楚地记得西柚一开始在绿桶里。两轮调用合计约十行代码,SimToM 即完整落地。

在 Agentic 工作流中的意义与已知局限

SimToM 最自然的应用场景是 Agentic 工作流:Agent 必须基于自己已知的信息采取行动,而不是利用全局可见的全部信息。视角代入阶段相当于给 Agent 显式划分"知识边界",使其在部分可观测环境中做出符合自身信息状态的决策。

需要说明的是,SimToM 并非没有代价:

  • 信息丢失风险:视角代入阶段可能过滤掉对后续问答至关重要的信息,导致最终答案错误;
  • 定位:正如论文作者所强调的,SimToM 更适合作为一个简单而有效的基线,用于评估 LLM 在 Theory of Mind 推理任务上的能力,而非声称是终极解决方案。

从示例到自定义:进一步探索

如果你想基于本示例做更多扩展,仓库提供了可继续深入研读的素材:

  • 模板与渲染的完整实现:src/outlines/templates.py,重点关注Template.from_filecreate_jinja_env
  • Generator 与 logits 处理器编译逻辑:src/outlines/generator.py,理解输出类型到受约束解码的完整链路;
  • transformers 集成的Transformers类与from_transformers工厂:src/outlines/models/transformers.py;
  • 两份可直接复用的 SimToM 提示词模板:docs/examples/prompt_templates/simtom_prospective_taking.txt 与 docs/examples/prompt_templates/simtom_simulation.txt;
  • 更多结构化生成实战示例:docs/examples/index.md(如chain_of_density.mdknowledge_graph_extraction.md等),以及源码级样例目录 examples(如react.pyself_consistency.py)。

若要在本地运行本示例,请先按 docs/guide/installation.md 安装 Outlines,并确保环境中包含transformerstorchpydantic依赖,之后将模板加载路径替换为仓库内实际路径即可端到端复现。

【免费下载链接】outlinesStructured Outputs项目地址: https://gitcode.com/GitHub_Trending/ou/outlines

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询