所有文章 > AI驱动 > LLM Agent之结构化输出

LLM Agent之结构化输出

引言

自去年年底以来,GPT 的迅速发展诞生了一系列大模型。出现了更新、更大、更强的 GPT-4。OpenAI 不断推出 GPT-4,ChatGPT Plugins,代码解释器,Function calling,图片处理等等。7月的 WAIC 上,笔者也有幸见到了国内一众企业相继展示自家的大模型。在这段时间里,LLM 从最初的 PE 工程走向智能体交互。而笔者从最开始考虑 LLM 能不能多人协作,思考“一个专家完成所有任务好还是很多人分工完成好”,到各种论文层出不穷,到如今火热的 LLM Agent 开发模式。可以说,如果你从大学里随便问某个人都知道 GPT,甚至大部分都用过。

好了,前言少叙。进入正题。众所周知,Agent 基本 = LLM(大型语言模型)+ 记忆 + 规划技能 + 工具使用。

想要使用工具,让 GPT 掌握如何使用工具,常见的方法是告知 GPT 工具(通常是一个可以调用的函数)的参数,让 GPT 生成这些参数即可。那么如何让 GPT 可靠的生成这些规定的参数呢?换一种说法,如何让 GPT 输出结构化的数据信息呢?

原理及相关框架

现如今大部分的结构化输出工具的原理都是:告诉 GPT 要输出一个怎么样的结构即可。没错~当然,为什么会出现这么多开发工具都用来解决这个问题,明明是一个简单的原理呢?

  1. 通过 prompt 告知 LLM 我们所需要的返回格式,并进行生成。
  2. 通过一些规则来检查返回结果,如果不符合格式,生成相关错误信息。
  3. 将上一次的生成内容和检查的错误信息告知 LLM,进行下一次的修正生成。
  4. 重复 2-3 步骤,直到生成的内容完全符合我们的要求。

首先,关于怎样描述这样一个结构的 Prompt 模板,众口难调。有些人认为结构就应该用自然语言描述,这样足够简单,上手难度足够低,方便快速迭代开发。有些人认为结构描述?JSON Schema不就够了?有些人觉得 YAML 也可以。有些人觉得上面这些对于我的需求还是够不着啊,于是自己造了一个伪代码描述。

其次,自动处理修正机制也可以做很多文章。还有许多对性能和开销的优化。

下文就是关于一众框架的简单分析。希望会对选择困难症的你有所帮助。

guardrails

guardrails 这个项目,就是将上述的步骤做了进一步的抽象与封装,提供更加 high level 的配置与 API 来完成整个过程。优点:

  1. 定义了一套RAIL spec
  2. 更聚焦于错误信息
<rail version="0.1">

<output>
<object name="patient_info">
<string name="gender" description="Patient's gender" />
<integer name="age" format="valid-range: 0 100" />
<list
name="symptoms"
description="Symptoms that the patient is currently experiencing. Each symptom should be classified into a separate item in the list.">
<object>
<string name="symptom" description="Symptom that a patient is experiencing"/>
<string
name="affected area"
description="What part of the body the symptom is affecting"
format="valid-choices: {['head', 'neck', 'chest']}"
on-fail-valid-choices="reask"
/>
</object>
</list>
<list name="current_meds" description="Medications the patient is currently taking and their response">
<object>
<string name="medication" description="Name of the medication the patient is taking" />
<string
name="response"
description="How the patient is responding to the medication"
/>
</object>
</list>
</object>
</output>

<prompt>

Given the following doctor's notes about a patient, please extract a dictionary that contains the patient's information.

{{doctors_notes}}

@complete_json_suffix_v2
</prompt>
</rail>

可以看到,guardrails 定义了一套类似 XML 的语言用于结构化输出,又结合了自然语言的 Prompt。虽然比起常见的模板语言要更加“繁琐”,但可以包含的内容也可以更加完善。比如可以提供字段的描述信息,检查规范,一定程度上也能帮助 LLM 更好地理解需求,生成预期的结果。

I was given the following JSON response, which had problems due to incorrect values.

{
"patient_info": {
"symptoms": [
{
"affected area": {
"incorrect_value": "face & hair",
"error_message": "Value face & hair is not in choices ['head', 'neck', 'chest']."
}
},
{
"affected area": {
"incorrect_value": "beard, eyebrows & nares",
"error_message": "Value beard, eyebrows & nares is not in choices ['head', 'neck', 'chest']."
}
}
]
}
}

Help me correct the incorrect values based on the given error messages.

后续 LLM 的返回可以仅针对这部分问题的修正,而不需要再重复生成整个 JSON。生成的新结果会由 guardrails 再自动填写回原来的位置,非常丝滑。除了 JSON 格式的检查外,RAIL spec 中还提供了通过脚本检查的扩展支持,可以用来检查更加复杂的内容,例如 Python 代码是否合法,结果中是否有敏感信息,甚至通过 LLM 再来检查生成的内容是否有害,做结果过滤等。

NeMo-Guardrails

来自 Nvidia 的一个同名项目,其目标相比 guardrails 更有野心,想要确保 LLM 应用整体的可信度,无害性以及数据安全性等,而不仅仅只是输出的结构化检查和修复。因此其实现思路上也复杂不少,设计了一种专门的 Colang 语言,来支持更加通用多样的业务流,而不仅仅是生成 -> 检查 -> 修复。不过它的设计都是基于对话做的。实际开发应用可能不太合适。

define user ask capabilities
"What can you do?"
"What can you help me with?"
"tell me what you can do"
"tell me about you"
"How can I use your help?"

define flow
user ask capabilities
bot inform capabilities

define bot inform capabilities
"I am an AI assistant which helps answer questions based on a given knowledge base. For this interaction, I can answer question based on the job report published by US Bureau of Labor Statistics."

从代码可以看出其结合了 Python 和自然语言,方便相似度检索。

其整体的运作流程如下:

  1. 根据用户输入识别用户意图。在这一步,系统会将用户的输入在 flow 定义的各种用户回复文本中做相似性查找,也就是上面文件中“What can you do?”这一连串内容。这些检索到的预设用户意图内容,结合其它信息如对话样例,最近聊天记录等,形成整体的 Prompt,发给 LLM 来生成回复。最终再从回复中提取用户意图。
  2. 根据意图,判断下一步操作动作。这一步有两种做法,一是当前的状态能够匹配上预定义的 flow。例如用户就是提了一个 bot 能力的问题,那么就会匹配上面定义的 user ask capabilities,下一步动作自然就是 bot inform capabilities。如果没有匹配上,就要由 LLM 自己来决定下一步动作,这时候也会跟生成用户意图一样,对于 flow 定义做一个相似性查找,将相关信息发给 LLM 来做生成。
  3. 生成 bot 回复。如果上一步生成的 bot 回复意图已经有明确定义了(例如上面的 bot 能力的回复),那么就直接用预定义的回复内容来回复用户。如果没有,就跟生成用户意图一样,做意图的相似性查找,将相关信息给 LLM 来生成回复。注意到很多动态的问题例如 QA 场景,是很难预先定义好回复内容的,这里也支持对接知识库,同样是做 vector search 之后,将相关 context 信息发给 LLM 来生成具体回复。

guidance

之前在 guardrails 中的做法是在 Prompt 中给出说明和示范,希望 LLM 能够遵循指令来输出。但现实中往往会出现各种问题,例如额外带了一些其它的文字说明,或者生成的 JSON 格式不正确等,所以需要后续的 ReAsk 来进行修正。LangChain 里也提供了各种 output parser 来帮忙提取回复中的结构化信息部分,但也经常容易运行失败。

在 guidance 中,也是通过“模板语言”来定义 LLM 的输出结构,以确保输出格式的正确性。

# load a model locally (we use LLaMA here)
guidance.llm = guidance.llms.Transformers("your_local_path/llama-7b", device=0)

# we can pre-define valid option sets
valid_weapons = ["sword", "axe", "mace", "spear", "bow", "crossbow"]

# define the prompt
program = guidance("""The following is a character profile for an RPG game in JSON format.
json
{
"description": "{{description}}",
"name": "{{gen 'name'}}",
"age": {{gen 'age' pattern='[0-9]+' stop=','}},
"armor": "{{#select 'armor'}}leather{{or}}chainmail{{or}}plate{{/select}}",
"weapon": "{{select 'weapon' options=valid_weapons}}",
"class": "{{gen 'class'}}",
"mantra": "{{gen 'mantra'}}",
"strength": {{gen 'strength' pattern='[0-9]+' stop=','}},
"items": [{{#geneach 'items' num_iterations=3}}
"{{gen 'this'}}",{{/geneach}}
]
}""")

# execute the prompt
program(description="A quick and nimble fighter.", valid_weapons=valid_weapons)

在之前传统的做法中,这一整个 JSON 都需要由 LLM 来生成。但是 JSON 的结构是我们预先定义的,例如有哪些字段,开闭的花括号等,其实都不需要 LLM 来生成。优点:

  1. 生成的 JSON 结构是保证合法且可控的,不会出现语法错误或者缺失/错误字段等。
  2. 通过 LLM 生成的 token 数量减少了,理论上可以提升生成速度。

除了 Prompt 模板,它还提供了:

  • 支持 hidden block,例如 LLM 的一些推理过程可能并不需要暴露给最终用户,就可以灵活利用这个特性来生成一些中间结果。
  • Generation caching,自动把已经生成过的结果缓存起来,提升速度。
  • 支持 HuggingFace 模型的 guidance acceleration,进一步提升生成速度。
  • Token healing,不看这个我还不知道 LLM 有这种问题……
  • Regex pattern guide,在模板的基础上进一步通过正则表达来限定生成的内容规范。

从项目代码来看,还是有比较浓的“research 味道”的,可读性并不好。实际测试结果也比较翻车。

lmql

在 guidance 的基础上,lmql 这个项目进一步把“Prompt 模板”这个概念推进到了一种新的编程语言。从官网能看到给出的一系列示例。语法结构看起来有点像 SQL,但函数与缩进都是 Python 的风格。

从支持的功能来看,相比 guidance 毫不逊色。例如各种限制条件,代码调用,各种 caching 加速,工具集成等基本都具备。这个框架的格式化输出是其次,其各种可控的输出及语言本身或许更值得关注。

TypeChat

TypeChat 将 Prompt 工程替换为 schema 工程:无需编写非结构化的自然语言 Prompt 来描述所需输出的格式,而是编写 TS 类型定义。TypeChat 可以帮助 LLM 以 JSON 的形式响应,并且响应结果非常合理:例如用户要求将这句话「我可以要一份蓝莓松饼和一杯特级拿铁咖啡吗?」转化成 JSON 格式,TypeChat 响应结果如下:

其本质原理是把 interface 之类的 ts 代码作为 Prompt 模板。因此它不仅可以对输出结果进行 ts校验,甚至能够输入注释描述,不可谓非常方便 js 开发者。不过,近日 typechat 爆火,很多开发者企图尝试将 typechat 移植到 Python,笔者认为这是缘木求鱼,因为其校验本身依赖的是 ts。笔者在开发过程中,将 typechat 融合到自己的库中,效果不错。但是它本身自带的 Prompt 和笔者输入的 Prompt 还是存在冲突,还是需要扣扣源码。

Langchain

如果你关注了过去几个月中人工智能的爆炸式发展,那你大概率听说过 LangChain。简单来说,LangChain 是一个 Python 和 JavaScript 库,由 Harrison Chase 开发,用于连接 OpenAI 的 GPT API(后续已扩展到更多模型)以生成人工智能文本。

LangChain 具有特别多的结构化输出工具。例如使用 yaml 定义 Schema,输出结构化 JSON。使用 zodSchema 定义 Schema,输出结构化 JSON。使用 FunctionParameters 定义 Schema,输出结构化 JSON

但是笔者这里不打算介绍 LangChain。究其原因,是笔者被 LangChain 折磨不堪。明明可以几行代码写清楚的东西,LangChain 可以各种封装,花了好几十行才写出来。更何况,笔者是用 ts 开发,开发时甚至偷不了任何懒,甚至其文档丝毫不友好。这几天,《机器之心》发布文章表示放弃 LangChain。要想让 LangChain 做笔者想让它做的事,就必须花大力气破解它,这将造成大量的技术负担。因为使用人工智能本身就需要花费足够的脑力。LangChain 是为数不多的在大多数常用情况下都会增加开销的软件之一。所以笔者建议非必要,不使用 LangChain。

LLM 对于结构化信息的理解

LLM 的可控性、稳定性、事实性、安全性等问题是推进企业级应用中非常关键的问题,上面分享的这些项目都是在这方面做了很多探索,也有很多值得借鉴的地方。总体思路上来说,主要是:

  • 提供一套 Prompt 模板定义,允许用户指定 LLM 生成的格式或内容主题。
  • 在模板基础上,也有不少项目进一步设计了相应的编程语言,让 LLM 与确定性程序的交互更加直观。
  • 提供各类 validator,保证生成内容符合预期,并且提供了自动处理/修正机制。
  • 更进一步,也可以在生成前进行干预,例如在 Prompt 中给近似案例,修改模型 decode 时的概率分布等。
  • 其它在可控性基础上做的各种性能与开销的优化,例如缓存,减少 token 消耗量,对开源模型能力的挖掘等。

即使我们不直接使用上述的项目做开发,也可以从中学习到很多有用的思路。当然也非常期待这个领域出现更多有意思的想法与研究,以及 Prompt 与编程语言结合能否碰撞出更多的火花。

同时笔者认为自动处理机制、自己设计的编程语言等等内容,随着时间发展,一定会层出不穷,不断迭代更新。笔者抛去这些时效性较弱的内容,从描述信息和位置信息两方面思考 Prompt 模板该如何设计,当然只是浅浅的抛砖引玉一下。

描述信息

到底哪种方式更容易于LLM去理解?我们不谈框架的设计,只考虑 Prompt 的设计。上述框架关于这方面有一些参考,例如有些直接拿 JSON 作为 Prompt模板,有些拿 xml 作为 Prompt 模板,有些拿自己设计的语言作为 Prompt,有些拿自然语言作为 Prompt 模板。时至今日,选用哪种最适合LLM去理解格式化的信息,输出格式化的内容完全没有盖棺定论。甚至时至今日,格式化输出问题还是没有得到可靠稳定的解决,要不然笔者肯定不会介绍这么多框架实践了。

笔者认为不管哪种方式,都可以从两个方面考量:更简单,更结构。如果想要在开发的时候更简单,或者在使用时更简单,选择 md、yaml 方式描述结构化信息更合适。如果想要更结构化的方式,选择 JSON、XML、TS,输出都能更有结构,甚至之后做结构校验都更方便。

想要 LLM 结构化输出更加稳定和理想,笔者认为选择 Prompt 模板时必须考虑每个字段是否有足够的辅助信息。例如 XML 描述时,每个标签都有一个描述属性描述这个标签时什么意思。

额外引申

笔者之前在开发 LLM 应用时,也曾思考类似的问题。笔者需要将多模态的数据进行结构化的标注,方便 LLM 去理解。但是标注成什么样却是一个很大的难题。笔者选择的是 JSON。但是,关于里面许多内容的标注。笔者在众多方案中徘徊。在细节处深挖,如何设计一种既简单,又能表示各种结构复杂关系,还能够节约 token 的方案及其的难。

位置信息

是否有人注意到 LLM 对于关键信息在 Prompt 中的位置会对结果产生影响呢?在设计 Prompt 方面,人们通常建议为语言模型提供详尽的任务描述和背景信息。近期的一些语言模型有能力输入较长的上下文,但它究竟能多好地利用更长的上下文?这一点却相对少有人知。近日,有学者研究发现如果上下文太长,语言模型会更关注其中的前后部分,中间部分却几乎被略过不看,导致模型难以找到放在输入上下文中部的相关信息。下文部分是该论文一些核心内容:

这是由其本身训练和结构设计有关的,但却对于我们开发有着莫大的帮助和指导意义。

相比之下,在多文档问答任务上,查询感知型上下文化的影响很小。特别指出,当相关信息位于输入上下文的最开始时,它可以提高性能,但在其他设置中会稍微降低性能。借此,我们可以认为,将重要的信息放在开头,结尾放置结构化模板,或许是一种优质选择。

那么如果真的为其提供这么多 token,那会真的有用吗?这个问题的答案是:由下游任务决定。因为这取决于所添加上下文的边际价值以及模型有效使用长输入上下文的能力。所以如果能有效地对检索文档排序(让相关信息与输入上下文的起始处更近)或对已排序的列表进行截断处理(必要时返回更少的文档),那么也许可以提升基于语言模型的阅读器使用检索上下文的能力。

参考资料:

https://mp.weixin.qq.com/s?__biz=MzA3MzI4MjgzMw==&mid=2650885029&idx=4&sn=ac01576a8957b41529dd3c877d262d5e&chksm=84e48fdbb39306cd8979a4fa7f7da14a9428dc28ccc47880d668ef6293b1a8b7b0964569ec36&mpshare=1&scene=23&srcid=0725w9FPsVnOOzkPGPB7lH8h&sharer_sharetime=1690303766527&sharer_shareid=d2396b329b12f49d34967e2b183540dd#rd

https://mp.weixin.qq.com/s/BngY2WgCcpTOlvdyBNJxqA

https://microsoft.github.io/TypeChat/

https://mp.weixin.qq.com/s?__biz=MzA3MzI4MjgzMw==&mid=2650885029&idx=4&sn=ac01576a8957b41529dd3c877d262d5e&chksm=84e48fdbb39306cd8979a4fa7f7da14a9428dc28ccc47880d668ef6293b1a8b7b0964569ec36&mpshare=1&scene=23&srcid=0725w9FPsVnOOzkPGPB7lH8h&sharer_sharetime=1690303766527&sharer_shareid=d2396b329b12f49d34967e2b183540dd#rd

本文章转载微信公众号@HelloTech技术派

#你可能也喜欢这些API文章!