结果解析
目录
背景
利用LLM构建应用的过程中,将 LLM 产生的字符串解析成指定的格式,提取出需要的信息,是一个非常重要的环节。 但同时由于下列原因,这个过程也是一个非常复杂的过程:
多样性:解析的目标格式多种多样,需要提取的信息可能是一段特定文本,一个JSON对象,或者是一个复杂的数据结构。
复杂性:结果解析不仅仅是将 LLM 产生的文本转换成目标格式,还涉及到提示工程(提醒 LLM 应该产生什么格式的输出),错误处理等一些列问题。
灵活性:同一个应用中,不同阶段也可能需要智能体产生不同格式的输出。
为了让开发者能够便捷、灵活的地进行结果解析,AgentScope设计并提供了解析器模块(Parser)。利用该模块,开发者可以通过简单的配置,实现目标格式的解析,同时可以灵活的切换解析的目标格式。
AgentScope中,解析器模块的设计原则是:
灵活:开发者可以灵活设置所需返回格式、灵活地切换解析器,实现不同格式的解析,而无需修改智能体类的代码,即具体的“目标格式”与智能体类内
reply
函数的处理逻辑解耦自由:用户可以自由选择是否使用解析器。解析器所提供的响应格式提示、解析结果等功能都是在
reply
函数内显式调用的,用户可以自由选择使用解析器或是自己实现代码实现结果解析透明:利用解析器时,提示(prompt)构建的过程和结果在
reply
函数内对开发者完全可见且透明,开发者可以精确调试自己的应用。
解析器模块
功能说明
解析器模块(Parser)的主要功能包括:
提供“响应格式说明”(format instruction),即提示 LLM 应该在什么位置产生什么输出,例如
You should generate python code in a fenced code block as follows
```python
{your_python_code}
```
提供解析函数(parse function),直接将 LLM 产生的文本解析成目标数据格式
针对字典格式的后处理功能。在将文本解析成字典后,其中不同的字段可能有不同的用处
AgentScope提供了多种不同解析器,开发者可以根据自己的需求进行选择。
目标格式 |
解析器 |
说明 |
---|---|---|
字符串( |
|
要求 LLM 将指定的文本生成到Markdown中以 ``` 标识的代码块中,解析结果为字符串。 |
字典( |
|
要求 LLM 在 ```json 和 ``` 标识的代码块中产生指定内容的字典,解析结果为 Python 字典。 |
|
要求 LLM 在多个标签中产生指定内容,这些不同标签中的内容将一同被解析成一个 Python 字典,并填入不同的键值对中。 |
|
|
适用于不确定标签名,不确定标签数量的场景。允许用户修改正则表达式,返回结果为字典。 |
|
JSON / Python对象类型 |
|
要求 LLM 在 ```json 和 ``` 标识的代码块中产生指定的内容,解析结果将通过 |
NOTE: 相比
MarkdownJsonDictParser
,MultiTaggedContentParser
更适合于模型能力不强,以及需要 LLM 返回内容过于复杂的情况。例如 LLM 返回 Python 代码,如果直接在字典中返回代码,那么 LLM 需要注意特殊字符的转义(\t,\n,…),json.loads
读取时对双引号和单引号的区分等问题。而MultiTaggedContentParser
实际是让大模型在每个单独的标签中返回各个键值,然后再将它们组成字典,从而降低了LLM返回的难度。
NOTE:AgentScope 内置的响应格式说明并不一定是最优的选择。在 AgentScope 中,开发者可以完全控制提示构建的过程,因此,选择不使用parser中内置的相应格式说明,而是自定义新的相应格式说明,或是实现新的parser类都是可行的技术方案。
下面我们将根据不同的目标格式,介绍这些解析器的用法。
字符串(str
)类型
MarkdownCodeBlockParser
初始化
MarkdownCodeBlockParser
采用 Markdown 代码块的形式,要求 LLM 将指定文本产生到指定的代码块中。可以通过language_name
参数指定不同的语言,从而利用大模型代码能力产生对应的输出。例如要求大模型产生 Python 代码时,初始化如下:from agentscope.parsers import MarkdownCodeBlockParser parser = MarkdownCodeBlockParser(language_name="python", content_hint="your python code")
响应格式模版
MarkdownCodeBlockParser
类提供如下的“响应格式说明”模版,在用户调用format_instruction
属性时,会将{language_name}
替换为初始化时输入的字符串:You should generate {language_name} code in a {language_name} fenced code block as follows: ```{language_name} {content_hint} ```
例如上述对
language_name
为"python"
的初始化,调用format_instruction
属性时,会返回如下字符串:print(parser.format_instruction)
You should generate python code in a python fenced code block as follows ```python your python code ```
解析函数
MarkdownCodeBlockParser
类提供parse
方法,用于解析LLM产生的文本,返回的是字符串。res = parser.parse( ModelResponse( text="""The following is generated python code ```python print("Hello world!") ``` """ ) ) print(res.parsed)
print("hello world!")
字典类型
关于 DictFilterMixin
与字符串和一般的 JSON / Python 对象不同,作为 LLM 应用中常用的数据格式,AgentScope 通过 DictFilterMixin
类为字典类型的解析提供后处理功能。
初始化解析器时,可以通过额外设置keys_to_content
,keys_to_memory
,keys_to_metadata
三个参数,从而实现在调用parser
的to_content
,to_memory
和to_metadata
方法时,对字典键值对的过滤。
其中
keys_to_content
指定的键值对将被放置在返回Msg
对象中的content
字段,这个字段内容将会被返回给其它智能体,参与到其他智能体的提示构建中,同时也会被self.speak
函数调用,用于显式输出keys_to_memory
指定的键值对将被存储到智能体的记忆中keys_to_metadata
指定的键值对将被放置在Msg
对象的metadata
字段,可以用于应用的控制流程判断,或挂载一些不需要返回给其它智能体的信息。
三个参数接收布尔值、字符串和字符串列表。其值的含义如下:
False
: 对应的过滤函数将返回None
。True
: 整个字典将被返回。str
: 对应的键值将被直接返回,注意返回的会是对应的值而非字典。List[str]
: 根据键值对列表返回过滤后的字典。
AgentScope中,keys_to_content
和 keys_to_memory
默认为 True
,即整个字典将被返回。keys_to_metadata
默认为 False
,即对应的过滤函数将返回 None
。
下面是狼人杀游戏的样例,在白天讨论过程中 LLM 扮演狼人产生的字典。在这个例子中,
"thought"
字段不应该返回给其它智能体,但是应该存储在智能体的记忆中,从而保证狼人策略的延续;"speak"
字段应该被返回给其它智能体,并且存储在智能体记忆中;"finish_discussion"
字段用于应用的控制流程中,判断讨论是否已经结束。为了节省token,该字段不应该被返回给其它的智能体,同时也不应该存储在智能体的记忆中。{ "thought": "The others didn't realize I was a werewolf. I should end the discussion soon.", "speak": "I agree with you.", "finish_discussion": True }
AgentScope中,我们通过调用to_content
,to_memory
和to_metadata
方法实现后处理功能,示意代码如下:
应用中的控制流代码,创建对应的解析器对象并装载
from agentscope.parsers import MarkdownJsonDictParser # ... agent = DictDialogAgent(...) # 以MarkdownJsonDictParser为例 parser = MarkdownJsonDictParser( content_hint={ "thought": "what you thought", "speak": "what you speak", "finish_discussion": "whether the discussion is finished" }, keys_to_content="speak", keys_to_memory=["thought", "speak"], keys_to_metadata=["finish_discussion"] ) # 装载解析器,即相当于指定了要求的相应格式 agent.set_parser(parser) # 讨论过程 while True: # ... x = agent(x) # 根据metadata字段,获取LLM对当前是否应该结束讨论的判断 if x.metadata["finish_discussion"]: break
智能体内部
reply
函数内实现字典的过滤# ... def reply(self, x: Optional[Union[Msg, Sequence[Msg]]] = None) -> Msg: # ... res = self.model(prompt, parse_func=self.parser.parse) # 过滤后拥有 thought 和 speak 字段的字典,存储到智能体记忆中 self.memory.add( Msg( self.name, content=self.parser.to_memory(res.parsed), role="assistant", ) ) # 存储到content中,同时存储到metadata中 msg = Msg( self.name, content=self.parser.to_content(res.parsed), role="assistant", metadata=self.parser.to_metadata(res.parsed), ) self.speak(msg) return msg
Note:
keys_to_content
,keys_to_memory
和keys_to_metadata
参数可以是列表,字符串,也可以是布尔值。
如果是
True
,则会直接返回整个字典,即不进行过滤如果是
False
,则会直接返回None
值如果是字符串类型,则
to_content
,to_memory
和to_metadata
方法将会把字符串对应的键值直接放入到对应的位置,例如keys_to_content="speak"
,则to_content
方法将会把res.parsed["speak"]
放入到Msg
对象的content
字段中,content
字段会是字符串而不是字典。如果是列表类型,则
to_content
,to_memory
和to_metadata
方法实现的将是过滤功能,对应过滤后的结果是字典parser = MarkdownJsonDictParser( content_hint={ "thought": "what you thought", "speak": "what you speak", }, keys_to_content="speak", keys_to_memory=["thought", "speak"], ) example_dict = {"thought": "abc", "speak": "def"} print(parser.to_content(example_dict)) # def print(parser.to_memory(example_dict)) # {"thought": "abc", "speak": "def"} print(parser.to_metadata(example_dict)) # Nonedef {"thought": "abc", "speak": "def"} None
解析器
针对字典类型的返回值,AgentScope 提供了多种不同的解析器,开发者可以根据自己的需求进行选择。
RegexTaggedContentParser
初始化
RegexTaggedContentParser
主要用于1)不确定的标签名,以及2)不确定标签数量的场景。在这种情况下,该解析器无法提供一个泛用性广的响应格式说明,因此需要开发者在初始化时提供对应的相应格式说明(format_instruction
)。
除此之外,用户可以通过设置try_parse_json
,required_keys
等参数,设置解析器的行为。
from agentscope.parsers import RegexTaggedContentParser
parser = RegexTaggedContentParser(
format_instruction="""Respond with specific tags as outlined below
<thought>what you thought</thought>
<speak>what you speak</speak>
""",
try_parse_json=True, # 尝试将标签内容解析成 JSON 对象
required_keys=["thought", "speak"] # 必须包含的键
)
MarkdownJsonDictParser
初始化 & 响应格式模版
MarkdownJsonDictParser
要求 LLM 在 ```json 和 ``` 标识的代码块中产生指定内容的字典。除了
to_content
,to_memory
和to_metadata
参数外,可以通过提供content_hint
参数提供响应结果样例和说明,即提示LLM应该产生什么样子的字典,该参数可以是字符串,也可以是字典,在构建响应格式提示的时候将会被自动转换成字符串进行拼接。from agentscope.parsers import MarkdownJsonDictParser # 字典 MarkdownJsonDictParser( content_hint={ "thought": "what you thought", "speak": "what you speak", } ) # 或字符串 MarkdownJsonDictParser( content_hint="""{ "thought": "what you thought", "speak": "what you speak", }""" )
对应的
instruction_format
属性
You should respond a json object in a json fenced code block as follows: ```json {content_hint} ```
类型校验
MarkdownJsonDictParser
中的content_hint
参数还支持基于Pydantic的类型校验。初始化时,可以将content_hint
设置为一个Pydantic的模型类,AgentScope将根据这个类来修改instruction_format
属性,并且利用Pydantic在解析时对LLM返回的字典进行类型校验。
该功能需要LLM能够理解JSON schema格式的提示,因此适用于能力较强的大模型。
一个简单的例子如下,"..."
处可以填写具体的类型校验规则,可以参考Pydantic文档。
from pydantic import BaseModel, Field
from agentscope.parsers import MarkdownJsonDictParser
class Schema(BaseModel):
thought: str = Field(..., description="what you thought")
speak: str = Field(..., description="what you speak")
end_discussion: bool = Field(..., description="whether the discussion is finished")
parser = MarkdownJsonDictParser(content_hint=Schema)
对应的
format_instruction
属性
Respond a JSON dictionary in a markdown's fenced code block as follows:
```json
{a_JSON_dictionary}
```
The generated JSON dictionary MUST follow this schema:
{'properties': {'speak': {'description': 'what you speak', 'title': 'Speak', 'type': 'string'}, 'thought': {'description': 'what you thought', 'title': 'Thought', 'type': 'string'}, 'end_discussion': {'description': 'whether the discussion reached an agreement or not', 'title': 'End Discussion', 'type': 'boolean'}}, 'required': ['speak', 'thought', 'end_discussion'], 'title': 'Schema', 'type': 'object'}
同时在解析的过程中,也将使用Pydantic进行类型校验,校验错误将抛出异常。同时,Pydantic也将提供一定的容错处理能力,例如将字符串
"true"
转换成Python的True
:
parser.parser("""
```json
{
"thought": "The others didn't realize I was a werewolf. I should end the discussion soon.",
"speak": "I agree with you.",
"end_discussion": "true"
}
```
""")
MultiTaggedContentParser
MultiTaggedContentParser
要求 LLM 在多个指定的标签对中产生指定的内容,这些不同标签的内容将一同被解析为一个 Python 字典。使用方法与MarkdownJsonDictParser
类似,只是初始化方法不同,更适合能力较弱的LLM,或是比较复杂的返回内容。
初始化 & 响应格式模版
MultiTaggedContentParser
中,每一组标签将会以TaggedContent
对象的形式传入,其中TaggedContent
对象包含了
标签名(
name
),即返回字典中的key值开始标签(
tag_begin
)标签内容提示(
content_hint
)结束标签(
tag_end
)内容解析功能(
parse_json
),默认为False
。当置为True
时,将在响应格式提示中自动添加提示,并且提取出的内容将通过json.loads
解析成 Python 对象
from agentscope.parsers import MultiTaggedContentParser, TaggedContent
parser = MultiTaggedContentParser(
TaggedContent(
name="thought",
tag_begin="[THOUGHT]",
content_hint="what you thought",
tag_end="[/THOUGHT]"
),
TaggedContent(
name="speak",
tag_begin="[SPEAK]",
content_hint="what you speak",
tag_end="[/SPEAK]"
),
TaggedContent(
name="finish_discussion",
tag_begin="[FINISH_DISCUSSION]",
content_hint="true/false, whether the discussion is finished",
tag_end="[/FINISH_DISCUSSION]",
parse_json=True, # 我们希望这个字段的内容直接被解析成 True 或 False 的 Python 布尔值
)
)
print(parser.format_instruction)
Respond with specific tags as outlined below, and the content between [FINISH_DISCUSSION] and [/FINISH_DISCUSSION] MUST be a JSON object:
[THOUGHT]what you thought[/THOUGHT]
[SPEAK]what you speak[/SPEAK]
[FINISH_DISCUSSION]true/false, whether the discussion is finished[/FINISH_DISCUSSION]
解析函数
MultiTaggedContentParser
的解析结果为字典,其中key为TaggedContent
对象的name
的值,以下是狼人杀中解析 LLM 返回的样例:
res_dict = parser.parse(
ModelResponse(text="""As a werewolf, I should keep pretending to be a villager
[THOUGHT]The others didn't realize I was a werewolf. I should end the discussion soon.[/THOUGHT]
[SPEAK]I agree with you.[/SPEAK]
[FINISH_DISCUSSION]true[/FINISH_DISCUSSION]
"""
)
)
print(res_dict)
{
"thought": "The others didn't realize I was a werewolf. I should end the discussion soon.",
"speak": "I agree with you.",
"finish_discussion": true
}
JSON / Python 对象类型
MarkdownJsonObjectParser
MarkdownJsonObjectParser
同样采用 Markdown 的```json和```标识,但是不限制解析的内容的类型,可以是列表,字典,数值,字符串等可以通过json.loads
进行解析字符串。
初始化 & 响应格式模版
from agentscope.parsers import MarkdownJsonObjectParser
parser = MarkdownJsonObjectParser(
content_hint="{A list of numbers.}"
)
print(parser.format_instruction)
You should respond a json object in a json fenced code block as follows:
```json
{a list of numbers}
```
解析函数
res = parser.parse(
ModelResponse(text="""Yes, here is the generated list
```json
[1,2,3,4,5]
```
"""
)
)
print(type(res))
print(res)
<class 'list'>
[1, 2, 3, 4, 5]
典型使用样例
狼人杀游戏
狼人杀(Werewolf)是字典解析器的一个经典使用场景,在游戏的不同阶段内,需要同一个智能体在不同阶段产生除了"thought"
和"speak"
外其它的标识字段,例如是否结束讨论,预言家是否使用能力,女巫是否使用解药和毒药,投票等。
AgentScope中已经内置了狼人杀的样例,该样例采用DictDialogAgent
类,配合不同的解析器,实现了灵活的目标格式切换。同时利用解析器的后处理功能,实现了“想”与“说”的分离,同时控制游戏流程的推进。
详细实现请参考狼人杀源码。
ReAct 智能体和工具使用
ReActAgent
是AgentScope中为了工具使用构建的智能体类,基于 ReAct 算法进行搭建,可以配合不同的工具函数进行使用。其中工具的调用,格式解析,采用了和解析器同样的实现思路。详细实现请参考代码。
自定义解析器
AgentScope中提供了解析器的基类ParserBase
,开发者可以通过继承该基类,并实现其中的format_instruction
属性和parse
方法来实现自己的解析器。
针对目标格式是字典类型的解析,可以额外继承agentscope.parser.DictFilterMixin
类实现对字典类型的后处理。
from abc import ABC, abstractmethod
from agentscope.models import ModelResponse
class ParserBase(ABC):
"""The base class for model response parser."""
format_instruction: str
"""The instruction for the response format."""
@abstractmethod
def parse(self, response: ModelResponse) -> ModelResponse:
"""Parse the response text to a specific object, and stored in the
parsed field of the response object."""
# ...