首页
学习
活动
专区
圈层
工具
发布
50 篇文章
1
解密Prompt系列1. Tunning-Free Prompt:GPT2 & GPT3 & LAMA & AutoPrompt
2
解密Prompt系列2. 冻结Prompt微调LM: T5 & PET & LM-BFF
3
解密Prompt系列3. 冻结LM微调Prompt: Prefix-tuning & Prompt-tuning & P-tuning
4
解密Prompt系列4. 升级Instruction Tuning:Flan/T0/InstructGPT/TKInstruct
5
​解密prompt系列5. APE+SELF=自动化指令集构建代码实现
6
解密Prompt系列6. lora指令微调扣细节-请冷静,1个小时真不够~
7
解密Prompt7. 偏好对齐RLHF-OpenAI·DeepMind·Anthropic对比分析
8
解密Prompt系列8. 无需训练让LLM支持超长输入:知识库 & Unlimiformer & PCW & NBCE
9
解密Prompt系列9. 模型复杂推理-思维链基础和进阶玩法
10
解密Prompt系列10. 思维链COT原理探究
11
​解密Prompt系列11. 小模型也能思维链推理
12
解密Prompt系列12. LLM Agent零微调范式 ReAct & Self Ask
13
解密Prompt系列13. LLM Agent指令微调方案: Toolformer & Gorilla
14
解密Prompt系列14. LLM Agent之搜索应用设计:WebGPT & WebGLM & WebCPM
15
解密Prompt系列15. LLM Agent之数据库应用设计:DIN & C3 & SQL-Palm & BIRD
16
解密Prompt系列16.LLM对齐经验之数据越少越好?LTD & LIMA & AlpaGasus
17
解密Prompt系列17. LLM对齐方案再升级 WizardLM & BackTranslation & SELF-ALIGN
18
解密Prompt系列18. LLM Agent之只有智能体的世界
19
解密Prompt系列19. LLM Agent之数据分析领域的应用:Data-Copilot & InsightPilot
20
解密Prompt系列20. LLM Agent 之再谈RAG的召回多样性优化
21
解密Prompt系列21. LLM Agent之再谈RAG的召回信息密度和质量
22
​解密Prompt系列22. LLM Agent之RAG的反思:放弃了压缩还是智能么?
23
解密Prompt系列23.大模型幻觉分类&归因&检测&缓解方案脑图全梳理
24
解密prompt24. RLHF新方案之训练策略:SLiC-HF & DPO & RRHF & RSO
25
解密prompt系列26. 人类思考vs模型思考:抽象和发散思维
26
解密prompt25. RLHF改良方案之样本标注:RLAIF & SALMON
27
解密prompt系列27. LLM对齐经验之如何降低通用能力损失
28
解密Prompt系列28. LLM Agent之金融领域智能体:FinMem & FinAgent
29
解密Prompt系列29. LLM Agent之真实世界海量API解决方案:ToolLLM & AnyTool
30
解密Prompt系列30. LLM Agent之互联网冲浪智能体们
31
​解密Prompt系列31. LLM Agent之从经验中不断学习的智能体
32
​解密Prompt系列33. LLM之图表理解任务-多模态篇
33
解密Prompt系列32. LLM之表格理解任务-文本模态
34
​解密prompt系列34. RLHF之训练另辟蹊径:循序渐进 & 青出于蓝
35
解密prompt系列35. 标准化Prompt进行时! DSPy论文串烧和代码示例
36
解密Prompt系列36. Prompt结构化编写和最优化算法UNIPROMPT
37
解密Prompt系列37.RAG之前置决策何时联网的多种策略
38
解密Prompt系列38.多Agent路由策略
39
解密prompt系列39. RAG之借助LLM优化精排环节
40
解密prompt系列40. LLM推理scaling Law
41
解密prompt系列41. GraphRAG真的是Silver Bullet?
42
解密prompt系列42. LLM通往动态复杂思维链之路
43
解密prompt系列43. LLM Self Critics
44
解密prompt系列44. RAG探索模式?深度思考模式?
45
解密Prompt45. 再探LLM Scalable Oversight -辩论、博弈哪家强
46
解密prompt系列46. LLM结构化输出代码示例和原理分析
47
解密prompt系列47. O1 Long Thought的一些特征分析
48
​解密prompt系列48. DeepSeek R1 & Kimi 1.5长思维链 - RL Scaling
49
​解密prompt系列49. 回顾R1之前的思维链发展
50
解密prompt系列50. RL用于优化Agent行为路径的一些思路

解密prompt系列46. LLM结构化输出代码示例和原理分析

最近闭源大模型们都陆续支持结构化输出,这一章我们先结合demo看下开源和闭源对结构化输出的支持,随后会介绍Constrained Decoding和Format Restricting Instructions 两种结构化输出约束方案,最后会给出结构化输出对比自然语言输出的一些观点。

代码示例

闭源 - OpenAI

https://platform.openai.com/docs/guides/structured-outputs/supported-schemas https://ai.google.dev/gemini-api/docs/json-mode?lang=python&hl=zh-cn

闭源三巨头都是支持结构化输出的,上面链了OpenAI和Gemini关于结构化输出的相关API文档。这里我们就以OpenAI为例,聊下结构化输出。

这里并非指OpenAI很早就支持的Json Mode,而JSON Mode的升级版Structure Output,只对gpt-4o-mini-2024-07-18和gpt-4o-2024-08-06之后的模型版本支持。简单说原来的JSON Mode只保证模型输出一个合法可以解析的json而已,对json的字段,字段类型,取值不做任何约束,而Strucutre Ouput则会进一步对JSON里面的具体字段和类型进行约束。这里我们举个例子,从基金季报中抽取基金经理对市场不同行业的观点,对观点进行情绪分类,并关联相关的申万一级行业。(哈哈并不是说这是最优的解决方案只是想把抽取,分类,生成任务融在一个case里面)

首先我们先定义抽取任务的结构体,申万一级行业的枚举值和情绪的枚举值,这里结构化输出都是使用pydantic定义的。通过枚举值定义我们可以约束模型输出的取值范围,而通过抽取结构定义我们可以约束模型输出的结构。不过这里对Enum的取值数量有限制一次输出的枚举值总量不能超过500,毕竟是直接作为模型上文,枚举值太多一是慢二是贵,三是不稳定。

代码语言:python
代码运行次数:0
复制
from enum import Enum
from typing import List
from pydantic import BaseModel, Field

class SWIndustry(Enum):
    AGRICULTURE = "农林牧渔"
    MINING = "采掘"
    CHEMICALS = "化工"
    STEEL = "钢铁"
    NONFERROUS_METALS = "有色金属"
    BUILDING_MATERIALS = "建筑材料"
    ELECTRICAL_EQUIPMENT = "电气设备"
    APPLIANCES = "家用电器"
    FOOD_BEVERAGE = "食品饮料"
    TEXTILE_APPAREL = "纺织服装"
    LIGHT_MANUFACTURING = "轻工制造"
    PHARMACEUTICALS = "医药生物"
    PUBLIC_UTILITIES = "公用事业"
    TRANSPORTATION = "交通运输"
    REAL_ESTATE = "房地产"
    COMMERCE_TRADE = "商业贸易"
    COMPUTER = "计算机"
    MEDIA = "传媒"
    COMMUNICATION = "通信"
    BANKING = "银行"
    NON_BANK_FINANCIAL = "非银金融"
    AUTOMOBILE = "汽车"
    MACHINERY = "机械设备"
    DEFENSE_MILITARY = "国防军工"
    BUILDING_CONSTRUCTION = "建筑装饰"
    ELECTRONICS = "电子"
    COMPREHENSIVE = "综合"
    LEISURE_SERVICES = "休闲服务"
    COMPUTER_APPLICATIONS = "计算机应用"
    CHEMICAL_FIBERS = "化纤"
    METAL_PRODUCTS = "金属制品"

class ViewAspect(Enum):
    POSITIVE = '正面'
    NETURAL = '中性'
    NEGATIVE = '负面'

    
class View(BaseModel):
    extract_view: str = Field(description="抽取文档中中对某个金融行业、或行业相关的主题或概念表达观点的句子")
    extract_view_entities: List[str] = Field(description="抽取观点金融主体,该主体必须出现在观点句子中,可以是金融行业,或行业相关概念或主题")
    related_industry: list[SWIndustry] = Field(description=f"观点金融主体最相关的1个或多个申万一级行业")
    view_aspect: ViewAspect = Field(description=f'对观点情绪进行精准分类,模糊情绪均为中性')

class ViewExtraction(BaseModel):
    views: list[View] = Field(
        ...,
        description="每个观点都应该是一个单独的对象,包含原文中表达观点的句子,观点主体,观点情绪分类和关联的申万一级行业",
    )

然后只需要把以上的结构体作为response_format的参数输入openai即可

代码语言:python
代码运行次数:0
复制
from openai import AzureOpenAI

client = AzureOpenAI(
    api_key = '...',
    api_version="2024-08-01-preview",
    azure_endpoint= "..."
)

completion = client.beta.chat.completions.parse(
    model='gpt-4o',
 messages=[
            {
                "role": "system",
                "content": "你是一个完美的金融观点解析系统,可以从文档中抽取观点,和观点对应主体并对观点进行分类。请从以下文档中抽取一系列观点",
            },
            {
                "role": "user",
                "content": content,
            },
        ],
    response_format= ViewExtraction
)

这样我们就能得到结构化的输出如下

再举一个function calling的例子,假设我们有两个工具一个Bing搜索,一个是基金信息查询工具,模型需要根据用户提问选择一个或多个工具来解决问题。

代码语言:python
代码运行次数:0
复制
from typing import Literal,Union,Optional
class BingSearch(BaseModel):
    query: str = Field(description="网页搜索query")
        
class FundInfo(BaseModel):
    """
    可以通过基金代码或基金名称,查询基金基础信息
    """
    fund_code_or_name: Optional[str] = Field(description="提问提及的基金代码或名称,没有则为空")
    lookup_field: Literal["fund_manager", "unit_value","contract_date","manage_fee","net_value"]

class Task(BaseModel):
    name: str = Field(description="任务名称")
    tool: Union[BingSearch, FundInfo] = Field(description="完成任务所需调用的工具")
        
class TaskSequence(BaseModel):
    reason: str = Field(description="先逐步思考要解决用户的问题需要哪些步骤")
    task_actions: List[Task] = Field(description="任务列表,按执行顺序依次排列")
   
completion = client.beta.chat.completions.parse(
    model='gpt-4o',
 messages=[
            {
                "role": "system",
                "content": "你是一个金融工具助手,可以完美根据用户提问选择需要调用的工具列表",
            },
            {
                "role": "user",
                "content": '天弘中证500当前的管理费是多少,是否随着基金规模的增加而增加',
            },
        ],
    response_format= TaskSequence
)

然后我们就能得到下面的结构化输出啦~

开源实现 - Instructor

https://github.com/jxnl/instructor https://github.com/PrefectHQ/marvin https://github.com/dottxt-ai/outlines

开源也有一些方案是针对结构化输出的,例如Instructor, Outlines。简单对比的话如果你用API调模型那Instructor更合适,如果你自己部署模型调用那Outlines更合适,vllm这些推理框架最新的版本也已经融入了Outlines。这里我们就选Instructor进行介绍。还是上面的例子,输出格式的定义相同,针对不满足openai版本条件的老模型,我们可以使用instructor来实现结构化输出。

代码语言:python
代码运行次数:0
复制
from openai import AzureOpenAI
import instructor
client = AzureOpenAI(
    api_key = '...',
    api_version="2024-08-01-preview",
    azure_endpoint= "..."
)
client = instructor.from_openai(client)

resp = client.chat.completions.create(
    model='thgpt4o',
    response_model= ViewExtraction,
    messages=[
        {
            "role": "system",
            "content": "你是一个完美的金融观点解析系统,可以从文档中抽取观点,和观点对应主体并对观点进行分类。请从以下文档中抽取一系列观点",
        },
        {
            "role": "user",
            "content": content,
        },
    ],
) 

那instructor,openai这些结构化输出能力都是如何实现的呢?下面我们来看几种约束模型给出结构化输出的方案

实现原理

这里提供两种不同的实现方案,一种是基于条件解码的强约束方案,和基于指令的弱约束方案,并且会给出不同方案对模型推理效果的影响。

Constrained Decoding

Efficient Guided Generation for Large Language https://github.com/dottxt-ai/outlines/tree/main

开源项目Outlines的两位作者Brandon T. Willard和R´emi Louf是比较早提出大模型可控生成方案的大佬。

条件解码方案其实就是在每一步解码时都对输出词表进行MASK(Regular Expression Guided Masking),只允许模型对当前位置符合输出格式的Token进行预测,把原始基于完整词表的softmax,转换成对于局部掩码词表的softmax。

那问题其实就简化成了在每一步推理时,如何选择该进行掩码的Token呢?毕竟GPT预测是自左向右,无法获得完整Token序列。论文把基于输出格式掩码的问题,转换成了基于有限状态机的状态转移问题(FSM)。简单解释下FSM其实就是由一组状态和状态之间的转移过程组成,词表中的字符满足条件的可以匹配到FSM的某个或某几个状态,从而在碰到字符A后,就可以确认几种满足条件的状态转移路径,从而根据后面的路径确认掩码词表。

因为词表中的每个字符究竟满足哪些状态,每个状态后有哪些可能的转移状态这些都是预先计算好的,因此并不需要在推理中动态计算,相反可以预先构建好每个词表到状态,再到后续转移状态的mapping。在解码过程中只需要根据解码字符读取mapping,对下一个字符进行对应的掩码即可。因此算法的时间复杂度是O(1),空间复杂度是O(状态数)。

这里我们还是举论文中的例子。我们的输出要求是满足浮点数“(0-9)?.?0-9”。这个输出约束可以被转换成FSM中的4种不同状态,每个状态有不同的转移状态(哈哈下面的例子是DeepSeek给大家举的)

  • 状态 0: 初始状态,可以进入状态1和2
  • 状态 1: 匹配数字 0-9,可以继续在状态1或者去状态2
  • 状态 2: 匹配小数点 .,只能进入状态3
  • 状态 3: 匹配小数点后的数字 0-9,可以继续在状态3

假设我们的词表只有5个字符{"A", ".", "42", ".2", "1"},那整个FSM掩码过程如下(以下词表选择过程是读取预先构建好的index)

  • 步骤 1:初始化 FSM。我们从状态 0 开始。
  • 步骤 2:查找当前状态下允许的词汇。在状态 0,根据 FSM,我们可以匹配数字 0-9 或小数点 .。因此,我们允许的词汇是:{".", "42", ".2", "1"}。
  • 步骤 3:选择一个词汇并更新 FSM 状态。假设我们选择了 ".2"。选择 ".2" 后,FSM 从状态 0 进入状态 2(因为匹配了小数点 .)。然后,FSM 继续匹配 "2",进入状态 3。
  • 步骤 4:继续生成下一个词汇。在状态 3,我们只能匹配数字 0-9。因此,允许的词汇是:{"42", "1"}。假设我们选择了 "1"。选择 "1" 后,FSM 保持在状态 3。
  • 步骤 5:生成结束。如果我们选择了一个表示结束的特殊词汇(如 EOS),生成过程结束。

基于已经构建好的FSM进行解码的步骤在Outlines里面如下(./generator/generatae.py)(哈哈下面的代码是cursor帮忙直接定位到的)

代码语言:python
代码运行次数:0
复制
def sequence_generator(model, sampler, fsms, token_ids, sequence_weights, attention_masks, fsm_states, rng):
    while True:
        # 1. 获取模型输出的logits
        logits, kv_cache = model(token_ids, attention_masks, kv_cache)
        
        # 2. 获取FSM允许的下一个token
        allowed_tokens = get_allowed_tokens(fsms, fsm_states)
        
        # 3. 基于allowed_tokens对logits进行mask,不允许的token均为-inf
        biased_logits = bias_logits(logits, allowed_tokens)
        
        # 4. 采样下一个token
        next_token_ids, ancestors, sequence_weights = sampler(biased_logits, sequence_weights, rng)
        
        # 5. 更新FSM状态
        fsm_states = get_next_fsm_states(fsms, fsm_states, next_token_ids)
        
        # 6. 检查是否生成完成
        is_finished = is_generation_finished(fsms, fsm_states)

Format Restricting Instructions

FRI是更简单的实现方案,也就是在指令中加入对应输出的约束。这里还是拿Instructor来举例子吧,虽然这并不准确,因为Instructor调用的API接口背后还是做了Constrained Decoding的逻辑,Instructor其实只是从中做了一层Adapter。但是不妨碍我们通过instructor的实现来看下如何把pydantic的定义转换成结构化输出的指令约束。

在上面使用instructor.from_openai(client)时,Instructor会打猴子补丁,在常规openai的接口上,增加response_model的预处理,和对输出的retry机制(patch.py)

代码语言:python
代码运行次数:0
复制
@overload
def from_openai(
    client: openai.OpenAI,
    mode: instructor.Mode = instructor.Mode.TOOLS,
    **kwargs: Any,
) -> Instructor:
    pass
    
@overload
def patch(
    client: OpenAI,
    mode: Mode = Mode.TOOLS,
) -> OpenAI: ...

def patch(  # type: ignore
    client: OpenAI | AsyncOpenAI | None = None,
    create: Callable[T_ParamSpec, T_Retval] | None = None,
    mode: Mode = Mode.TOOLS,
) -> OpenAI | AsyncOpenAI:
    """
    Patch the `client.chat.completions.create` method

    Enables the following features:

    - `response_model` parameter to parse the response from OpenAI's API
    - `max_retries` parameter to retry the function if the response is not valid
    - `validation_context` parameter to validate the response using the pydantic model
    - `strict` parameter to use strict json parsing
    - `hooks` parameter to hook into the completion process
    """

    logger.debug(f"Patching `client.chat.completions.create` with {mode=}")

    if create is not None:
        func = create
    elif client is not None:
        func = client.chat.completions.create
    else:
        raise ValueError("Either client or create must be provided")

    @wraps(func)  # type: ignore
    def new_create_sync(
        response_model: type[T_Model] | None = None,
        validation_context: dict[str, Any] | None = None,
        context: dict[str, Any] | None = None,
        max_retries: int | Retrying = 1,
        strict: bool = True,
        hooks: Hooks | None = None,
        *args: T_ParamSpec.args,
        **kwargs: T_ParamSpec.kwargs,
    ) -> T_Model:
        context = handle_context(context, validation_context)

        response_model, new_kwargs = handle_response_model(
            response_model=response_model, mode=mode, **kwargs
        )

        new_kwargs = handle_templating(new_kwargs, context)

        response = retry_sync(
            func=func,  # type: ignore
            response_model=response_model,
            context=context,
            max_retries=max_retries,
            args=args,
            hooks=hooks,
            strict=strict,
            kwargs=new_kwargs,
            mode=mode,
        )
        return response  # type: ignore
    new_create = new_create_async if func_is_async else new_create_sync
    if client is not None:
        client.chat.completions.create = new_create  # type: ignore
        return client
    else:
        return new_create  # type: ignore

其中handle_response_model的部分会针对不同模型的API接口进行不同的指令处理,上面使用OpenAI时使用了工具调用模式来实现结构化输出。

代码语言:python
代码运行次数:0
复制
def openai_schema(cls) -> dict[str, Any]:
    """
    Return the schema in the format of OpenAI's schema as jsonschema

    Note:
        Its important to add a docstring to describe how to best use this class, it will be included in the description attribute and be part of the prompt.

    Returns:
        model_json_schema (dict): A dictionary in the format of OpenAI's schema as jsonschema
    """
    schema = cls.model_json_schema()
    docstring = parse(cls.__doc__ or "")
    parameters = {
        k: v for k, v in schema.items() if k not in ("title", "description")
    }
    for param in docstring.params:
        if (name := param.arg_name) in parameters["properties"] and (
            description := param.description
        ):
            if "description" not in parameters["properties"][name]:
                parameters["properties"][name]["description"] = description

    parameters["required"] = sorted(
        k for k, v in parameters["properties"].items() if "default" not in v
    )

    if "description" not in schema:
        if docstring.short_description:
            schema["description"] = docstring.short_description
        else:
            schema["description"] = (
                f"Correctly extracted `{cls.__name__}` with all "
                f"the required parameters with correct types"
            )

    return {
        "name": schema["title"],
        "description": schema["description"],
        "parameters": parameters,
    }

拿前面基金Function Call的例子来说,实际进入GPT模型的指令被转换成了以下函数调用的指令格式

代码语言:json
复制
{'name': 'TaskSequence',
 'description': 'Correctly extracted `TaskSequence` with all the required parameters with correct types',
 'parameters': {'$defs': {'BingSearch': {'properties': {'query': {'description': '网页搜索query',
      'title': 'Query',
      'type': 'string'}},
    'required': ['query'],
    'title': 'BingSearch',
    'type': 'object'},
   'FundInfo': {'description': '可以通过基金代码或基金名称,查询基金基础信息',
    'properties': {'fund_code_or_name': {'anyOf': [{'type': 'string'},
       {'type': 'null'}],
      'description': '提问提及的基金代码或名称,没有则为空',
      'title': 'Fund Code Or Name'},
     'lookup_field': {'enum': ['fund_manager',
       'unit_value',
       'contract_date',
       'manage_fee',
       'net_value'],
      'title': 'Lookup Field',
      'type': 'string'}},
    'required': ['fund_code_or_name', 'lookup_field'],
    'title': 'FundInfo',
    'type': 'object'},
   'Task': {'properties': {'name': {'description': '任务名称',
      'title': 'Name',
      'type': 'string'},
     'tool': {'anyOf': [{'$ref': '#/$defs/BingSearch'},
       {'$ref': '#/$defs/FundInfo'}],
      'description': '完成任务所需调用的工具',
      'title': 'Tool'}},
    'required': ['name', 'tool'],
    'title': 'Task',
    'type': 'object'}},
  'properties': {'reason': {'description': '先逐步思考要解决用户的问题需要哪些步骤',
    'title': 'Reason',
    'type': 'string'},
   'task_actions': {'description': '任务列表,按执行顺序依次排列',
    'items': {'$ref': '#/$defs/Task'},
    'title': 'Task Actions',
    'type': 'array'}},
  'required': ['reason', 'task_actions'],
  'type': 'object'}}

FRI缺少严格约束,所以只能依赖模型的指令遵从能力,有一定概率输出结果会无法还原成原始的的Pydantic类型。下面我们看另一种强约束的方案。

优劣对比

Let Me Speak Freely? A Study on the Impact of Format Restrictions on Performance of Large Language Models https://blog.dottxt.co/say-what-you-mean.html

针对上述的两种结构化解码方案,对比常规的自然语言推理对模型效果的影响几何?我先是读到的第一篇论文(Let Me Speak Freely),核心结论其实是结构化输出会影响模型的推理效果。

但是随后Outlines的作者们就发了一篇博客指出了论文的几个核心问题。双方各自站的立场不同,但逻辑上个博客指出的几个论文的核心问题确实很有说服力,包括

  • 论文使用自然语言推理和使用结构化输出推理的指令不同,因此效果不可比
  • 论文使用了第二个大模型对结构化输出的结果进行解析(引入了更多错误),实际上正确的使用方式应该是直接使用推理输出来还原pydantic model即可,毕竟大家使用结构化输出的其中一个原因就是更好解析。
  • 论文使用的结构化输出prompt质量有待提升

博客给出的最终结论是在GSM8k,Last Letter,Shuffled Object这三个任务上结构化输出相比NL输出都有提升。并且直接给出了基于Outlines的结果复现代码github repo(这里强烈建议大家去瞅瞅上面的博客,对于结构化输出有些很有意思的见解)

但是吸取前面盲目偏信前一篇论文的教训,其实在平时的任务尝试上,个人感觉结构化输出的效果和具体任务,Prompt(fewshot)质量,模型本身的指令能力强相关。因此还是倾向于在应用时充分对比NL和Structure的效果后再做应用。在大模型时代很多结论都有领域和模型局限性,大家需要在自己的场景上审慎判断哈哈~

“新年伊始,愿各位代码如诗行云流水,bug如朝露见光即散;创意如泉涌,论文如宝藏,实验如神助,成功率百分百!科研路上,你我皆是‘码’到成功的幸运儿!🎉”

想看更全的大模型论文·微调预训练数据·开源框架·AIGC应用 >> DecryPrompt

下一篇
举报
领券