公司动态
解密prompt系列46. LLM结构化输出代码示例和原理分析
前言最近闭源大模型们都陆续支持结构化输出这一章我们先结合demo看下开源和闭源对结构化输出的支持随后会介绍Constrained Decoding和Format Restricting Instructions 两种结构化输出约束方案最后会给出结构化输出对比自然语言输出的一些观点。代码示例闭源 - OpenAIhttps://platform.openai.com/docs/guides/structured-outputs/supported-schemashttps://ai.google.dev/gemini-api/docs/json-mode?langpythonhlzh-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毕竟是直接作为模型上文枚举值太多一是慢二是贵三是不稳定。fromenumimportEnumfromtypingimportListfrompydanticimportBaseModel,FieldclassSWIndustry(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金属制品classViewAspect(Enum):POSITIVE正面NETURAL中性NEGATIVE负面classView(BaseModel):extract_view:strField(description抽取文档中中对某个金融行业、或行业相关的主题或概念表达观点的句子)extract_view_entities:List[str]Field(description抽取观点金融主体,该主体必须出现在观点句子中可以是金融行业或行业相关概念或主题)related_industry:list[SWIndustry]Field(descriptionf观点金融主体最相关的1个或多个申万一级行业)view_aspect:ViewAspectField(descriptionf对观点情绪进行精准分类模糊情绪均为中性)classViewExtraction(BaseModel):views:list[View]Field(...,description每个观点都应该是一个单独的对象包含原文中表达观点的句子观点主体观点情绪分类和关联的申万一级行业,)然后只需要把以上的结构体作为response_format的参数输入openai即可fromopenaiimportAzureOpenAI clientAzureOpenAI(api_key...,api_version2024-08-01-preview,azure_endpoint...)completionclient.beta.chat.completions.parse(modelgpt-4o,messages[{role:system,content:你是一个完美的金融观点解析系统可以从文档中抽取观点和观点对应主体并对观点进行分类。请从以下文档中抽取一系列观点,},{role:user,content:content,},],response_formatViewExtraction)这样我们就能得到结构化的输出如下再举一个function calling的例子假设我们有两个工具一个Bing搜索一个是基金信息查询工具模型需要根据用户提问选择一个或多个工具来解决问题。fromtypingimportLiteral,Union,OptionalclassBingSearch(BaseModel):query:strField(description网页搜索query)classFundInfo(BaseModel): 可以通过基金代码或基金名称查询基金基础信息 fund_code_or_name:Optional[str]Field(description提问提及的基金代码或名称没有则为空)lookup_field:Literal[fund_manager,unit_value,contract_date,manage_fee,net_value]classTask(BaseModel):name:strField(description任务名称)tool:Union[BingSearch,FundInfo]Field(description完成任务所需调用的工具)classTaskSequence(BaseModel):reason:strField(description先逐步思考要解决用户的问题需要哪些步骤)task_actions:List[Task]Field(description任务列表按执行顺序依次排列)completionclient.beta.chat.completions.parse(modelgpt-4o,messages[{role:system,content:你是一个金融工具助手可以完美根据用户提问选择需要调用的工具列表,},{role:user,content:天弘中证500当前的管理费是多少是否随着基金规模的增加而增加,},],response_formatTaskSequence)然后我们就能得到下面的结构化输出啦~开源实现 - Instructorhttps://github.com/jxnl/instructorhttps://github.com/PrefectHQ/marvinhttps://github.com/dottxt-ai/outlines开源也有一些方案是针对结构化输出的例如Instructor, Outlines。简单对比的话如果你用API调模型那Instructor更合适如果你自己部署模型调用那Outlines更合适vllm这些推理框架最新的版本也已经融入了Outlines。这里我们就选Instructor进行介绍。还是上面的例子输出格式的定义相同针对不满足openai版本条件的老模型我们可以使用instructor来实现结构化输出。fromopenaiimportAzureOpenAIimportinstructor clientAzureOpenAI(api_key...,api_version2024-08-01-preview,azure_endpoint...)clientinstructor.from_openai(client)respclient.chat.completions.create(modelthgpt4o,response_modelViewExtraction,messages[{role:system,content:你是一个完美的金融观点解析系统可以从文档中抽取观点和观点对应主体并对观点进行分类。请从以下文档中抽取一系列观点,},{role:user,content:content,},],)那instructoropenai这些结构化输出能力都是如何实现的呢下面我们来看几种约束模型给出结构化输出的方案实现原理这里提供两种不同的实现方案一种是基于条件解码的强约束方案和基于指令的弱约束方案并且会给出不同方案对模型推理效果的影响。Constrained DecodingEfficient Guided Generation for Large Languagehttps://github.com/dottxt-ai/outlines/tree/main开源项目Outlines的两位作者Brandon T. Willard和R´emi Louf是比较早提出大模型可控生成方案的大佬。条件解码方案其实就是在每一步解码时都对输出词表进行MASKRegular Expression Guided Masking只允许模型对当前位置符合输出格式的Token进行预测把原始基于完整词表的softmax转换成对于局部掩码词表的softmax。那问题其实就简化成了在每一步推理时如何选择该进行掩码的Token呢毕竟GPT预测是自左向右无法获得完整Token序列。论文把基于输出格式掩码的问题转换成了基于有限状态机的状态转移问题FSM。简单解释下FSM其实就是由一组状态和状态之间的转移过程组成词表中的字符满足条件的可以匹配到FSM的某个或某几个状态从而在碰到字符A后就可以确认几种满足条件的状态转移路径从而根据后面的路径确认掩码词表。因为词表中的每个字符究竟满足哪些状态每个状态后有哪些可能的转移状态这些都是预先计算好的因此并不需要在推理中动态计算相反可以预先构建好每个词表到状态再到后续转移状态的mapping。在解码过程中只需要根据解码字符读取mapping对下一个字符进行对应的掩码即可。因此算法的时间复杂度是O1空间复杂度是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帮忙直接定位到的defsequence_generator(model,sampler,fsms,token_ids,sequence_weights,attention_masks,fsm_states,rng):whileTrue:# 1. 获取模型输出的logitslogits,kv_cachemodel(token_ids,attention_masks,kv_cache)# 2. 获取FSM允许的下一个tokenallowed_tokensget_allowed_tokens(fsms,fsm_states)# 3. 基于allowed_tokens对logits进行mask,不允许的token均为-infbiased_logitsbias_logits(logits,allowed_tokens)# 4. 采样下一个tokennext_token_ids,ancestors,sequence_weightssampler(biased_logits,sequence_weights,rng)# 5. 更新FSM状态fsm_statesget_next_fsm_states(fsms,fsm_states,next_token_ids)# 6. 检查是否生成完成is_finishedis_generation_finished(fsms,fsm_states)Format Restricting InstructionsFRI是更简单的实现方案也就是在指令中加入对应输出的约束。这里还是拿Instructor来举例子吧虽然这并不准确因为Instructor调用的API接口背后还是做了Constrained Decoding的逻辑Instructor其实只是从中做了一层Adapter。但是不妨碍我们通过instructor的实现来看下如何把pydantic的定义转换成结构化输出的指令约束。在上面使用instructor.from_openai(client)时Instructor会打猴子补丁在常规openai的接口上增加response_model的预处理和对输出的retry机制patch.py)overloaddeffrom_openai(client:openai.OpenAI,mode:instructor.Modeinstructor.Mode.TOOLS,**kwargs:Any,)-Instructor:passoverloaddefpatch(client:OpenAI,mode:ModeMode.TOOLS,)-OpenAI:...defpatch(# type: ignoreclient:OpenAI|AsyncOpenAI|NoneNone,create:Callable[T_ParamSpec,T_Retval]|NoneNone,mode:ModeMode.TOOLS,)-OpenAI|AsyncOpenAI: Patch the client.chat.completions.create method Enables the following features: - response_model parameter to parse the response from OpenAIs 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(fPatching client.chat.completions.create with{mode})ifcreateisnotNone:funccreateelifclientisnotNone:funcclient.chat.completions.createelse:raiseValueError(Either client or create must be provided)wraps(func)# type: ignoredefnew_create_sync(response_model:type[T_Model]|NoneNone,validation_context:dict[str,Any]|NoneNone,context:dict[str,Any]|NoneNone,max_retries:int|Retrying1,strict:boolTrue,hooks:Hooks|NoneNone,*args:T_ParamSpec.args,**kwargs:T_ParamSpec.kwargs,)-T_Model:contexthandle_context(context,validation_context)response_model,new_kwargshandle_response_model(response_modelresponse_model,modemode,**kwargs)new_kwargshandle_templating(new_kwargs,context)responseretry_sync(funcfunc,# type: ignoreresponse_modelresponse_model,contextcontext,max_retriesmax_retries,argsargs,hookshooks,strictstrict,kwargsnew_kwargs,modemode,)returnresponse# type: ignorenew_createnew_create_asynciffunc_is_asyncelsenew_create_syncifclientisnotNone:client.chat.completions.createnew_create# type: ignorereturnclientelse:returnnew_create# type: ignore其中handle_response_model的部分会针对不同模型的API接口进行不同的指令处理上面使用OpenAI时使用了工具调用模式来实现结构化输出。defopenai_schema(cls)-dict[str,Any]: Return the schema in the format of OpenAIs 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 OpenAIs schema as jsonschema schemacls.model_json_schema()docstringparse(cls.__doc__or)parameters{k:vfork,vinschema.items()ifknotin(title,description)}forparamindocstring.params:if(name:param.arg_name)inparameters[properties]and(description:param.description):ifdescriptionnotinparameters[properties][name]:parameters[properties][name][description]description parameters[required]sorted(kfork,vinparameters[properties].items()ifdefaultnotinv)ifdescriptionnotinschema:ifdocstring.short_description:schema[description]docstring.short_descriptionelse:schema[description](fCorrectly extracted {cls.__name__} with all fthe required parameters with correct types)return{name:schema[title],description:schema[description],parameters:parameters,}拿前面基金Function Call的例子来说实际进入GPT模型的指令被转换成了以下函数调用的指令格式{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 Modelshttps://blog.dottxt.co/say-what-you-mean.html针对上述的两种结构化解码方案对比常规的自然语言推理对模型效果的影响几何我先是读到的第一篇论文Let Me Speak Freely核心结论其实是结构化输出会影响模型的推理效果。但是随后Outlines的作者们就发了一篇博客指出了论文的几个核心问题。双方各自站的立场不同但逻辑上个博客指出的几个论文的核心问题确实很有说服力包括论文使用自然语言推理和使用结构化输出推理的指令不同因此效果不可比论文使用了第二个大模型对结构化输出的结果进行解析引入了更多错误实际上正确的使用方式应该是直接使用推理输出来还原pydantic model即可毕竟大家使用结构化输出的其中一个原因就是更好解析。论文使用的结构化输出prompt质量有待提升博客给出的最终结论是在GSM8kLast LetterShuffled Object这三个任务上结构化输出相比NL输出都有提升。并且直接给出了基于Outlines的结果复现代码github repo这里强烈建议大家去瞅瞅上面的博客对于结构化输出有些很有意思的见解但是吸取前面盲目偏信前一篇论文的教训其实在平时的任务尝试上个人感觉结构化输出的效果和具体任务Promptfewshot质量模型本身的指令能力强相关。因此还是倾向于在应用时充分对比NL和Structure的效果后再做应用。在大模型时代很多结论都有领域和模型局限性大家需要在自己的场景上审慎判断哈哈~最后的最后感谢你们的阅读和喜欢作为一位在一线互联网行业奋斗多年的老兵我深知在这个瞬息万变的技术领域中持续学习和进步的重要性。为了帮助更多热爱技术、渴望成长的朋友我特别整理了一份涵盖大模型领域的宝贵资料集。这些资料不仅是我多年积累的心血结晶也是我在行业一线实战经验的总结。这些学习资料不仅深入浅出而且非常实用让大家系统而高效地掌握AI大模型的各个知识点。如果你愿意花时间沉下心来学习相信它们一定能为你提供实质性的帮助。这份完整版的大模型 AI 学习资料已经上传CSDN朋友们如果需要可以微信扫描下方CSDN官方认证二维码免费领取【保证100%免费】大模型知识脑图为了成为更好的 AI大模型 开发者这里为大家提供了总的路线图。它的用处就在于你可以按照上面的知识点去找对应的学习资源保证自己学得较为全面。经典书籍阅读阅读AI大模型经典书籍可以帮助读者提高技术水平开拓视野掌握核心技术提高解决问题的能力同时也可以借鉴他人的经验。对于想要深入学习AI大模型开发的读者来说阅读经典书籍是非常有必要的。实战案例光学理论是没用的要学会跟着一起敲要动手实操才能将自己的所学运用到实际当中去这时候可以搞点实战案例来学习。面试资料我们学习AI大模型必然是想找到高薪的工作下面这些面试题都是总结当前最新、最热、最高频的面试题并且每道题都有详细的答案面试前刷完这套面试题资料小小offer不在话下640套AI大模型报告合集这套包含640份报告的合集涵盖了AI大模型的理论研究、技术实现、行业应用等多个方面。无论您是科研人员、工程师还是对AI大模型感兴趣的爱好者这套报告合集都将为您提供宝贵的信息和启示。这份完整版的大模型 AI 学习资料已经上传CSDN朋友们如果需要可以微信扫描下方CSDN官方认证二维码免费领取【保证100%免费】