结果解析

目录

背景

利用LLM构建应用的过程中,将 LLM 产生的字符串解析成指定的格式,提取出需要的信息,是一个非常重要的环节。 但同时由于下列原因,这个过程也是一个非常复杂的过程:

  1. 多样性:解析的目标格式多种多样,需要提取的信息可能是一段特定文本,一个JSON对象,或者是一个复杂的数据结构。

  2. 复杂性:结果解析不仅仅是将 LLM 产生的文本转换成目标格式,还涉及到提示工程(提醒 LLM 应该产生什么格式的输出),错误处理等一些列问题。

  3. 灵活性:同一个应用中,不同阶段也可能需要智能体产生不同格式的输出。

为了让开发者能够便捷、灵活的地进行结果解析,AgentScope设计并提供了解析器模块(Parser)。利用该模块,开发者可以通过简单的配置,实现目标格式的解析,同时可以灵活的切换解析的目标格式。

AgentScope中,解析器模块的设计原则是:

  1. 灵活:开发者可以灵活设置所需返回格式、灵活地切换解析器,实现不同格式的解析,而无需修改智能体类的代码,即具体的“目标格式”与智能体类内reply函数的处理逻辑解耦

  2. 自由:用户可以自由选择是否使用解析器。解析器所提供的响应格式提示、解析结果等功能都是在reply函数内显式调用的,用户可以自由选择使用解析器或是自己实现代码实现结果解析

  3. 透明:利用解析器时,提示(prompt)构建的过程和结果在reply函数内对开发者完全可见且透明,开发者可以精确调试自己的应用。

解析器模块

功能说明

解析器模块(Parser)的主要功能包括:

  1. 提供“响应格式说明”(format instruction),即提示 LLM 应该在什么位置产生什么输出,例如

You should generate python code in a fenced code block as follows
```python
{your_python_code}
```
  1. 提供解析函数(parse function),直接将 LLM 产生的文本解析成目标数据格式

  2. 针对字典格式的后处理功能。在将文本解析成字典后,其中不同的字段可能有不同的用处

AgentScope提供了多种不同解析器,开发者可以根据自己的需求进行选择。

目标格式

解析器

说明

字符串(str)类型

MarkdownCodeBlockParser

要求 LLM 将指定的文本生成到Markdown中以 ``` 标识的代码块中,解析结果为字符串。

字典(dict)类型

MarkdownJsonDictParser

要求 LLM 在 ```json 和 ``` 标识的代码块中产生指定内容的字典,解析结果为 Python 字典。

MultiTaggedContentParser

要求 LLM 在多个标签中产生指定内容,这些不同标签中的内容将一同被解析成一个 Python 字典,并填入不同的键值对中。

RegexTaggedContentParser

适用于不确定标签名,不确定标签数量的场景。允许用户修改正则表达式,返回结果为字典。

JSON / Python对象类型

MarkdownJsonObjectParser

要求 LLM 在 ```json 和 ``` 标识的代码块中产生指定的内容,解析结果将通过 json.loads 转换成 Python 对象。

NOTE: 相比MarkdownJsonDictParserMultiTaggedContentParser更适合于模型能力不强,以及需要 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_contentkeys_to_memorykeys_to_metadata三个参数,从而实现在调用parserto_contentto_memoryto_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_contentkeys_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_contentto_memoryto_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_contentkeys_to_memorykeys_to_metadata参数可以是列表,字符串,也可以是布尔值。

  • 如果是True,则会直接返回整个字典,即不进行过滤

  • 如果是False,则会直接返回None

  • 如果是字符串类型,则to_contentto_memoryto_metadata方法将会把字符串对应的键值直接放入到对应的位置,例如keys_to_content="speak",则to_content方法将会把res.parsed["speak"]放入到Msg对象的content字段中,content字段会是字符串而不是字典。

  • 如果是列表类型,则to_contentto_memoryto_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)) # None
    
    def
    {"thought": "abc", "speak": "def"}
    None
    

解析器

针对字典类型的返回值,AgentScope 提供了多种不同的解析器,开发者可以根据自己的需求进行选择。

RegexTaggedContentParser
初始化

RegexTaggedContentParser 主要用于1)不确定的标签名,以及2)不确定标签数量的场景。在这种情况下,该解析器无法提供一个泛用性广的响应格式说明,因此需要开发者在初始化时提供对应的相应格式说明(format_instruction)。 除此之外,用户可以通过设置try_parse_jsonrequired_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_contentto_memoryto_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."""

    # ...