亚马逊AWS官方博客

用亚马逊 Bedrock Agent 构建智能体和知识库 – 金蝶发票云票据助手开发实践(第一篇:Agent 开发实践)

目前,基于大语言模型的应用开发通常集中在两个方向:检索增强生成(RAG)和代理(Agent)。但无论是哪个方向,构建基于大模型的应用仅依赖 FM(基础模型)还远远不够,我们还需要集成多种组件,比如需要多种类型的数据库以便保存历史会话、向量数据;需要引入基于 FM 的应用开发框架(如 Langchain)简化开发;需要构建并优化提示词来保证 Agent Tools 准确调用,这无形中提升了开发门槛,拖慢了应用的上线步伐。

亚马逊在刚刚结束的 re:Invent 2023 发布大会上新推出的 Bedrock Agent 同时提供了 RAG 和 Agent 两种能力,通过提示词的自动构建、内置的历史会话管理、与 Lambda、多种向量库的自动集成,极大加速了企业用户对生成式 AI 的应用程序开发进程。Bedrock Agent 帮助用户更加专注于自身的业务,基于自身公司 API 和知识库来快速构建企业专用的智能体应用。

有关 Bedrock agent 原理/实现的博客已有官方发布,本博客从代码、实操层面出发,带领大家基于 Bedrock Agent 从 0 到 1 快速实现智能体构建。场景是金蝶发票云探索基于基础大模型构建面向企业用户的发票助手,希望从以前用户需要面向系统做填单操作来开发票转变为通过自然语言对话申请发票开具。该 Agent 可以实现如下两个功能:

  • 向客户收集开发票所需要的信息,自动调用后端 API 为用户开具发票
  • 基于企业票据知识回答用户提问

本博客是系列博客的第一部分。将介绍如何基于 Bedrock agent 构建发票开具助手:

用户向 Agent 提出对话请求,Agent 接收后结合 Bedrock Agent 平台预置的提示词模版生成更加准确完善的提示词,交由 Bedrock 上提供的 FM 进行推理,根据 FM  返回的结果,进行下一步的行为规划。根据上下文信息以及当前的执行结果,决定是否需要向客户收集信息/根据已有信息调用 API/直接给出回答。

系列博客的第二部分将介绍如何在 Agent 中使用 Bedrock Knowledge base 添加企业的私域知识库,执行私域问答。让我们开始吧!

下图概括了构建 Bedrock Agent 所需要的基本组件。其中红色部分为开发人员需要投入精力完成编码的部分,绿色部分为平台已经提供的功能。

创建 Bedrock Agent 的主要开发工作如下:

  1. 首先需要编写 Agent 的工作指令。这是构建 Agent 的重要部分。该指令需要详细描述了 Agent 应该做什么以及应该如何与用户互动。Agent 就是基于这个描述来确定对客户请求处理流程;以及如何使用提供的 Action group 和 Knowledge base 工具完成请求。
  2. 然后构建一个或多个 Action groups。每个 Action groups 对应一个 Lambda 函数和相应的 API schema 描述。Lambda 中包含用于工具执行的多个函数定义。API schema 用于描述 Lambda 函数中每个 function 的作用、参数意义;对 FM 基于 API schema 做出 Function 的选择以及参数的收集,因此 API schema 定义的准确性也非常重要。
  3. 另外就是 Knowledge base 的创建,Bedrock agent 提供了内置的 Embedding 模型和向量库,帮助用户快速构建私域知识库。

下面我们先从开发 Agent 可以使用的工具开始入手。Bedrock Agent 实现了与 Lambda 的集成,通过 Lambda 完成工具的开发。

1. 使用 Lambda 创建 Tools

与 Lambda 集成体现了使用 Bedrock agent 的开发优势。Bedrock agent 使用 FM 的推理能力识别用户的请求意图,在判定需要 function 调用后可以自动收集所需的所有参数,然后通过 Lambda 调用相应的 function。对 Lambda 的开发仅需要包含 Tools 的功能开发,非常简单。而如果使用 Langchain 自行构建 agent 时,需要添加 Langchain 依赖库。由于 Langchain 开源版本变化非常快,依赖库的版本更新、与 FM 版本的匹配性等问题都给我们后续的开发、维护带来难度。Bedrock agent 通过 AWS 平台负责维护,不需要客户负责这部分内容。

下面我们展示构建 Lambda tools 的详细开发过程。

1.1 使用 Layer 创建 Lambda 函数的依赖

首先使用 Lambda Layer 创建函数所需要的依赖包。在本次项目中我们构建的 Lambda 工具包实现了:

  • 根据用户提供的发票信息生成票据预览图(generatePreviewInvoiceImage 工具函数)
  • pdf 根据用户提供的发票信息生成正式的发票文件(issueInvoice 工具函数)
  • 根据用户提供的邮件地址使用亚马逊的 SES 服务发送邮件(sendInvoiceEmail 工具函数)。

首先,我们根据这两个函数需要的依赖库构建 Lambda Layer:本次 Lambda 的 runtime 使用 python 3.11,使用 lambda image 安装依赖库。

mkdir layer
cp requirements.txt layer/requirements.txt
docker run -ti -v $(pwd)/layer:/app -w /app --entrypoint /bin/bash public.ecr.aws/lambda/python:3.11 -c "pip3 install --taget ./agentToolsLib -r requirements.txt"
zip -r agentToolsLib.zip agentToolsLib

上传 agentToolsLib.zip 到 lambda_layer

构建 Lambda Layer 更详细的信息也可以参考官方文档: https://docs.aws.amazon.com/lambda/latest/dg/python-package.html

为了让 Lambda 能够成功访问 Layer,注意在创建 Lambda 之后,环境变量配置加入 PYTHONPATH=/opt/agentToolsLib

1.2 创建 Lambda 函数

1.2.1 创建 Lambda

  • 创建 Lambda 函数,指定所需要的 runtime:11,以及所需的基本权限
  • 对该 Lambda 函数添加刚才创建名字为 agentToolsLib 的 Layer

1.2.2 Lambda 函数 Event 内容解析

LLM 在判断需要使用 Tools 时会触发 Lambda,产生触发事件具有特定的结构。Event 关键内容包含:

  • 需要调用的 function 名称:
    如”apiPath”: “/issueInvoice”
  • 执行函数所需要的参数:
    “parameters”: [ ….]

以下为调用 issueInvoice 工具时产生的 Agent Event 示例。 Agent Event 详情请参考官方文档 https://docs.aws.amazon.com/zh_cn/bedrock/latest/userguide/agents-create.html

{
    "agent": {
        "alias": "TSTALIASID",
        "name": "KingdeeAgent",
        "version": "DRAFT",
        "id": "NORJI2BS5I"
    },
    "sessionId": "955513527673393",
    "inputText": "公司名称:优方网络,识别号:91440300MA5FAE9E4P, 用户ID:300,产品信息: 小麦,1010101020000000000,9000",
    "sessionAttributes": {},
    "promptSessionAttributes": {},
    "apiPath": "/issueInvoice",
    "httpMethod": "POST",
    "messageVersion": "1.0",
    "actionGroup": "issueInvoice",
    "parameters": [
        {
            "name": "buyer_company_name",
            "type": "string",
            "value": "优方网络"
        },
        {
            "name": "product_detail",
            "type": "array",
            "value": "[\"{\"name\":\"小麦\",\"code\":\"1010101020000000000\",\"money\":\"9000\"}\"]"
        },
        {
            "name": "user_id",
            "type": "string",
            "value": "300"
        },
        {
            "name": "buyer_tax_number",
            "type": "string",
            "value": "91440300MA5FAE9E4P"
        }
    ]
}

Lambda 函数编写时可以参考如下代码解析 Event,获取函数的相关参数。

def get_named_parameter(event, name):
    return next(item for item in event['parameters'] if item['name'] == name)['value']

1.2.3 Lambda 函数编写

Lambda 函数编写分两部分。

  • 第一部分为客户根据业务需求做相应业务函数的编写。如下面 generatePreviewInvoiceImage、sendInvoiceEmail、issueInvoice 等函数编写。
  • 第二部分为 Lambda 入口函数编写,根据 event 中携带的 api_path 调用相应的业务函数。
def sendInvoiceEmail(event):
    invoice_code = get_named_parameter(event, 'invoice_code') 
    invoice_number = get_named_parameter(event, 'invoice_number')
    email_address = get_named_parameter(event, 'email_address')
	  ## --------处理逻辑省略-------------- ##
    result = send_eamil(email_address, "/tmp/invoice.pdf")
    #定义输出
    res = {}
    res["input_args"] = {}
    res["input_args"]["invoice_code"] = invoice_code
    res["input_args"]["invoice_number"] = invoice_number
    res["input_args"]["email_address"] = email_address
    if result["errcode"] == "0000":
        res["status"] = "success"
        res["results"] = "邮件发送成功"
    else:
        res["status"] = "fail"
        res["results"] = "邮件发送失败,请稍后尝试重新发送."
    return res

def generatePreviewInvoiceImage(event):
  ## 处理逻辑省略 ##
def issueInvoice(event):
  ## 处理逻辑省略 ##
def lambda_handler(event, context):
    result = ''
    response_code = 200
    ## 解析触发事件,根据apiPath调用相应的工具函数 ##
    action_group = event['actionGroup']
    api_path = event['apiPath']
 
		if api_path == '/issueInvoice':
        result = issueInvoice(event)
    elif api_path == '/sendInvoiceEmail':
        result = sendInvoiceEmail(event) 
    else:
        response_code = 404
        result = f"Unrecognized api path: {action_group}::{api_path}"

    response_body = {
        'application/json': {
            'body': json.dumps(result)
        }
    }
    ##Bedrock Agent需要Lambda函数具有固定的返回值模版 ##
    session_attributes = event['sessionAttributes']
    prompt_session_attributes = event['promptSessionAttributes']
    action_response = {
        'actionGroup': event['actionGroup'],
        'apiPath': event['apiPath'],
        # 'httpMethod': event['HTTPMETHOD'], 
        'httpMethod': event['httpMethod'], 
        'httpStatusCode': response_code,
        'responseBody': response_body,
        'sessionAttributes': session_attributes,
        'promptSessionAttributes': prompt_session_attributes
    }
    api_response = {'messageVersion': '1.0', 'response': action_response}   
    return api_response

Lambda 函数的返回值格式,请参考如下示例并结合官方文档 https://docs.aws.amazon.com/zh_cn/bedrock/latest/userguide/agents-create.html

在使用 Lambda 创建完所需要的 Tools 后就可以在 Bedrock 上构建 Agent 了。

2. Bedrock Agent 的构建

2.1 Agent 指令的编写

前面说到,Agent 指令的编写非常重要,它决定了 Agent 应该做什么以及应该如何与用户互动。开发人员需要根据实际需求构建自己的 Agent 指令,没有固定的模版。但通常可以包含如下的内容:

  1. 工作流程或步骤
  2. 清晰描述业务逻辑,每一步具体需要实现什么
  3. 返回给客户的内容

下面的例子中给出本项目中构建发票助手使用的 Agent 指令参考:

您好,我是友好的发票助手。当收到问候时,我会礼貌地回复。我通过“issueInvoice” action group 提供开具发票服务。在生成发票时,我会首先从用户那里收集所有所需的发票信息,然后为用户生成临时预览图以供参考。如果成功生成了预览图像,我会从返回结果中获得发票金额、发票类型、购买方名称给用户。在生成实际发票之前,我会与用户确认是否要继续。如果用户确认,我会使用工具生成发票。如果成功,我会从函数结果中返回 downloadUrl 给用户,以便他们下载发票。如果用户表示信息不正确,我会要求他们提供更正后的信息。我还会确认用户是否需要把发票发送到指定的电子邮箱。如果要求发送,我会把发票文件发送到提供的邮箱。

该指令定义了几个关键内容:

  • 描述了 Agent 中包含的 Action groups 的功能:如 issueInvoice Action groups 是用于提供发票服务的
  • 定义了开发票的主要流程:收集开发票的所需信息 ->产生预览文件->开具正式发票->将发票发送至邮箱。注意业务描述尽量与对应 API 的描述语意一致
  • 定义了返回给用户对话内容:例如,如果正式发票成功生成,将函数返回中的 downloadUrl 返回给客户以便客户下载查看

2.2 创建 Action groups

创建 Agent 的 action group,除了 Lambda 的编写,还需要提供该 action group 对应的 API schema。Bedrock agent 提供了两种方式进行开发:

  1. 编写自己的 json 描述,上传 S3
  2. 提供了 in-line 的 OpenAPI editor,可以直接在线编写

API Schema 的具体要求参见官方文档 https://docs.aws.amazon.com/zh_cn/bedrock/latest/userguide/agents-create.html

https://github.com/OAI/OpenAPI-Specification/tree/main/examples/v3.0 中还能找到具体示例。

下面给出了发票助手 InvoiceService_ActionGroup 的 API schema 构建示例:

{
    "openapi": "3.0.0",
    "info": {
      "title": "InvoiceService API",
      "description": "APIs for invoice service",  
      "version": "1.0.0"
    },
    "paths": {
      "/generatePreviewInvoiceImage": {
        "post": {
          "description": "Generate a temporary preview invoice image.",
          "operationId": "generatePreviewInvoiceImage",
          "parameters": [
            {
              "name": "user_id",
              "in": "query",
              "description": "Id of user",
              "required": true,
              "schema": {
                "type": "string",
                "default": "000001"
              }
            },
            {
              "name": "product_detail",
              "in": "query",
              "description": "'name','code','money' for the product",
              "required": true,
              "schema": {
                "type": "array",
                "items": {
                    "type": "dict"
                }
              },
              "example": [
                {"name": "实木茶几","code": "1050201010000000000", "money": 1000},
                {"name": "餐饮费用", "code": "3070401000000000000", "money": 500}
              ]
            },
            {
              "name": "buyer_company_name",
              "in": "query",
              "description": "The name of buyer company",
              "required": true,
              "schema": {
                "type": "string"
              },
              "example": "广东唯一网络科技有限公司"
            },
            {
                "name": "buyer_tax_number",
                "in": "query",
                "description": "The tax number of buyer company",
                "required": true,
                "schema": {
                  "type": "string"
                },
                "example": "91450923MA5L7W2C1W"
              },
            {
              "name": "invoice_type",
              "in": "query",
              "description": "The type of invoice",
              "schema": {
                "type": "string",
                "default": "全电普通发票",
                "enum": ["全电普通发票","全电专用发票"]
              }
            },
            {
                "name": "remark",
                "in": "query",
                "description": "Remarks on the invoice",
                "schema": {
                  "type": "string"
                }
              }
          ],
          
          "responses": {
            "200": {
              "description": "Generate a temporary preview invoice image successfully",
              "content": {
                "application/json": {
                  "schema": {
                    "type": "object",
                    "properties": {
                      "status": {
                        "type": "string"
                      },
                      "results": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      },

2.3 创建 Agent

在上述准备工作全部完成后,我们就可以创建 Agent 了:

  1. 提供 Agent 的基本信息
  2. 选择 Agent 使用的 FM,并填写 Agent 指令。目前可以选择的 FM 包括 Claude instant V1 和 Claude V2。对 1 的支持未来即将上线
  3. 创建 Action group,对每个 Action 提供对应的 Lambda 以及 API schema

对 KnowledgeBase 的构建我们留待下一篇博客。这里对 Knowledge base 的构建暂且跳过。

2.4 编辑 Lambda 资源策略,允许 Agent 访问

最后,为了能让 Agent 可以成功调用 Action group 中定义的 Lambda,还需要编辑 Lambda 资源策略,允许 Agent 访问:

  1. 创建好 Agent 之后,获得 Agent 的 ARN
  2. 在 Lambda 上构建资源策略,允许 Agent 访问

3. 功能测试

最后,我们使用 Bedrock agent 在 console 上提供的测试功能,直接对 Agent 进行测试。

如下视频已经包含了 Bedrock Agent 和 Bedrock Knowledge Base 的全部效果。对 Knowledge Base 的开发详情请参考第二篇博客。

Demo 代码参考:https://github.com/xiaoqunnaws/bedrock_agent_konwledege_base

本篇作者

倪惠青

亚马逊云科技解决方案架构师,负责基于 AWS 云计算方案架构的咨询和设计,在国内推广 AWS 云平台技术和各种解决方案。在加入 AWS 之前曾在 Oracle,Microsoft 工作多年,负责企业公有云方案咨询和架构设计,在基础架构及大数据方面有丰富经验。

董孝群

亚马逊云科技解决方案架构师,GCR GenAI SSA,负责生成式 AI 解决方案的设计,曾在百度,粤港澳大湾区数字经济研究院供职,在 nlp 领域有着丰富经验,目前专注于大语言模型 PE 应用开发。

林丽敏

金蝶票据云科技(深圳)有限公司担任算法工程师,主攻方向自然语言处理(NLP),专注于采用人工智能技术解决业务问题以及进行产品和服务的创新。