为了账号安全,请及时绑定邮箱和手机立即绑定

如何用AI端到端测试AI回复:一份实战指南

我们使用亚马逊Bedrock测试框架对AI响应进行无缝的端到端测试,确保测试的信心、语气和断言的准确性,并且示例使用Typescript和AWS CDK编写。

前言

✔️ 我们讨论了测试AI提示输出的必要性。
✔️ 我们讨论了断言语句、置信度评分和评估。
✔️ 我们讨论了API响应的确定性需求。
✔️ 我们讲解了AWS CDK和TypeScript中的代码示例。

嘿👋🏽

我们都有过类似的经历,通过亚马逊Bedrock或其他生成式AI工具生成文本内容,但当我们向客户展示这些内容时,如何才能对这些回复感到放心呢?我们怎么知道这些回复是否靠谱呢?我们怎么知道里面有没有关键信息呢?

在这篇文章中,我将教你如何使用一个为 Jest 和 Typescript 编写的 AI 测试框架来实现这一点。为了使这个案例更贴近现实,我们将讨论一个虚构的公司叫‘Gilmore Garage’,它:

允许客户通过API访问他们的车辆信息。

✔️ 客服人员会呼叫汽车检测的第三方API,并用非技术性的语言为顾客简单解释检测结果。

您可以在GitHub上找到完整的代码示例: https://github.com/

GitHub - leegilmorecode/integration-testing-amazon-bedrock: 我们提供覆盖无缝端到端的 AI 响应测试,使用 Amazon Bedrock 测试框架用于信心、语气等方面的测试……github.com

👇 在我们继续之前,请免费注册我的免费 Serverless Advocate 通讯,获取技巧和提示、新文章、社区最新动态、新的 AWS 服务等更多内容:

Serverless Advocate 通讯简报 | Substack欢迎订阅 Serverless Advocate 通讯简报!在这里,您可以每周获取最新的新闻和文章…serverlessadvocate.substack.com

首先,让我们看看在下一节里我们需要解决的测试生成AI回复时遇到的问题。

测试AI生成的回复为什么难? 🧠

使用像 Amazon Bedrock 这样的服务生成内容时,测试生成输出的主要困难在于输出的非确定性。

如果我们考虑一下常见的测试场景,我们会编写函数,确保给定相同的输入时,它们总是输出相同的结果。这样我们就可以运行类似的测试。

function generateFullName(firstName: string, lastName: string): string {  
  return `${firstName} ${lastName}`;  
}  

describe('generate-full-name', () => {,  
  测试生成完整的名字(() => {  
    const fullName = generateFullName('John', 'Doe');  
    expect(fullName).toBe('John Doe');  
  });  
});

谈到AI模型生成的输出时,每次运行测试套件时,响应都可能不一样。这是测试生成响应时的主要难题,因为我们没有一个具体值可以断言(或像前面提到的Jest,预期结果一样)。

我们来再加点复杂度!

火上浇油的是,我们很可能在调用另一个服务、一组 API 或类似的东西,这些返回的数据会被我们用AI生成内容。今天我们也会看到这种情况的发生。这意味着在进行从头到尾的测试时,底层数据也会变得不稳定。

简单来说:

通常在进行端到端测试时,生成式AI响应依赖的数据经常变化。

AI模型生成的响应每次模型运行时很少相同,这使得测试方法上与通常的断言测试方法不同。

那我们在本文中该如何开始呢?

✔️ 我们使用一个API 测试框架来确保在将数据传递给 AI 生成摘要时,数据现在变得稳定。

✔️ 我们现在使用AI来测试AI生成的输出;检查语调、文本中的断言以及置信度分数,这让我们可以用Jest运行测试套件。

让我们在下一节看看我们要建的。

我们在建啥 ⚙️

为了亲自展示这个问题,并看看我们如何解决这些问题,以及彻底测试响应,我们来看一下以下的解决方案架构:

从上面的图表中我们可以看到:

  1. 客户可以使用客户服务REST API来获取他们的MOT车辆检测报告的摘要。
  2. 客户使用的REST API(API网关)调用一个Lambda函数,该函数调用外部车库的REST API以获取特定客户的检测详情。
  3. Lambda函数使用从车库API获取的原始JSON响应,使用Amazon Bedrock生成一个面向客户的非技术性报告摘要,并将生成的报告返回给客户。

当我们进行客户API的端到端测试(e2e)时,我们希望确保AI生成的摘要与我们无法控制的车库API响应的一致性,并且我们知道生成的输出每次可能会有所不同。

从上面的e2e测试可以看到:

  1. 我们使用Jest运行测试套件,它会像预期的那样对面向公众的客户API运行端到端(e2e)测试。
  2. 我们的Lambda函数现在使用我们的API测试套件,而不是具体的车库REST API实现,这意味着我们现在可以返回测试场景所需的正确车辆检查JSON。
  3. Amazon Bedrock现在基于测试套件的响应生成检查摘要,由于我们提供了底层数据,生成的结果是确定的,我们现在使用我们的AI测试框架测试生成的摘要输出的完整性、语气以及每个测试的置信度评分。

我们现在来看看下一节里的核心代码。

👇 在我们继续之前 — 请在我的 LinkedIn 上连接我,以便了解未来的博客文章和 (Serverless)[无服务器] 新闻 https://www.linkedin.com/in/lee-james-gilmore/

不通过关键代码交流,改为:通过键码聊天👨‍💻

我们将在这里讨论三个不同的部分,服务部分、测试环境和测试库部分。

服务

好的,让我们开始讨论,当客户API通过其REST API请求带有特定ID的检查请求时,车库API的回复。

车库-api/stateless/src/use-cases/get-mot/get-mot.ts

    import { MotResult } from '@dto/mot-result';
    import { faker } from '@faker-js/faker';
    import { logger } from '@shared';

    export async function getMotUseCase(id: string): Promise<MotResult> {
      // 注意:在这个示例中,我们使用 Faker 而不是从我们的车库 API 获取示例 API 数据来创建数据库。然而,这模仿了数据会随着时间变化且不可预测的事实。
      const generateRandomMotResult = (id: string): MotResult => ({
        id,
        created: new Date().toISOString(),
        updated: new Date().toISOString(),
        vehicle: {
          registration: faker.vehicle.vrm(),
          make: faker.vehicle.manufacturer(),
          model: faker.vehicle.model(),
          color: faker.vehicle.color(),
          yearOfManufacture: faker.number.int({ min: 2000, max: 2023 }),
        },
        motResult: {
          testDate: faker.date.past().toISOString().split('T')[0],
          expiryDate: faker.date.future().toISOString().split('T')[0],
          result: faker.helpers.arrayElement(['PASS', 'FAIL']),
          testCenter: {
            name: faker.company.name(),
            location: {
              addressLine1: faker.location.streetAddress(),
              town: faker.location.city(),
              postcode: faker.location.zipCode(),
            },
          },
          mileage: faker.number.int({ min: 10000, max: 100000 }),
          defects: faker.helpers.arrayElements(
            [
              {
                code: 'D001',
                description: '刹车片磨损低于最低厚度',
              },
              { code: 'D002', description: '前照灯照射角度过高' },
            ],
            faker.number.int({ min: 0, max: 2 }),
          ),
          advisories: faker.helpers.arrayElements(
            [
              {
                code: 'A001',
                description: '近侧前轮胎磨损接近法律最低标准',
              },
              { code: 'A002', description: '转向横拉杆接头轻微松动或磨损' },
            ],
            faker.number.int({ min: 0, max: 2 }),
          ),
        },
      });

      const motResult = generateRandomMotResult(id);

      logger.info(
        `已记录 mot 结果检索信息:${JSON.stringify(motResult)},id 为 ${id}`,
      );

      return motResult;
    }

从上面的代码中可以看出,我想要让从车库API获取的数据非常不稳定,每次调用时都生成不同的API响应!我们使用faker来实现这一点。

💡 提示:这通常会存放在数据库里,我只是在说明这一点而已。

这意味着现在每次请求都会生成不同的响应,下面是一个 JSON 响应示例:

{
    "id": "1",
    "created": "2024-11-01T15:43:07.967Z",
    "updated": "2024-11-01T15:43:07.967Z",
    "vehicle": {
        "registration": "JP84MGU",
        "make": "Kia",
        "model": "阿提默",
        "color": "薄荷绿色",
        "yearOfManufacture": 2003
    },
    "motResult": {
        "testDate": "2024-03-30",
        "expiryDate": "2025-06-11",
        "result": "不合格",
        "testCenter": {
            "name": "海德里希",
            "location": {
                "addressLine1": "地址1",
                "town": "城市",
                "postcode": "79341"
            }
        },
        "mileage": 35036,
        "defects": [
            {
                "code": "D002",
                "description": "前照灯对准过高了"
            },
            {
                "code": "D001",
                "description": "刹车片磨损低于最低限度"
            }
        ],
        "advisories": []
    }
}

💡 小提示:上述回复是用于AI模型生成客户服务摘要回复的一个示例。

当我们的客户现在访问客户API接口时,我们的Lambda函数将被如下调用。

客户API/stateless/src/use-cases/get-result/get-result.ts

    import {  
      ModelResponses,  
      invokeBedrockApi,  
    } from '@adapters/secondary/bedrock-adapter/bedrock.adapter';  

    import { getCustomerCommunicationPreferences } from '@adapters/secondary/database-adapter';  
    import { httpCall } from '@adapters/secondary/http-adapter';  
    import { config } from '@config';  
    import { ActionPromptDto } from '@dto/action-prompt';  
    import { Response } from '@dto/response';  
    import { Result } from '@dto/result';  
    import { logger } from '@shared';  
    import { getRestEndpoint } from '@shared/get-rest-endpoint';  

    const stage = config.get('stage');  
    const modelId = config.get('bedrockModelId');  
    const bedrockVersion = config.get('bedrockVersion');  

    export async function getResultUseCase(  
      id: string,  
      customerId: string,  
    ): Promise<Response> {  
      // 获取正确的URL,即garage API或测试平台端点,取决于stage
      const restEndpoint = await getRestEndpoint(stage);  

      // 获取该客户的沟通偏好
      const communicationPreference =  
        await getCustomerCommunicationPreferences(customerId);  

      // 调用API以获取特定客户的MOT结果信息
      const motResult = (await httpCall(  
        restEndpoint,  
        `v1/results/${id}`,  
        'GET',  
      )) as Result;  

      logger.debug(  
        `MOT结果信息已获取: ${JSON.stringify(motResult)} for id ${id}`,  
      );  

      // 通过Bedrock使用AI和客户的沟通偏好生成客户摘要
      const params: ActionPromptDto = {  
        prompt: `Human:请总结一份适合非技术人员且喜好为${communicationPreference}的客户理解的车辆检测结果,其中结果详情为${JSON.stringify(motResult)},摘要应包含车辆颜色、品牌、型号、检测日期、有效期截止日期、检测中心名称及地址,以及任何注意事项。请直接给出结果,不要特意说明这是非技术人员的总结。Assistant:`,  
        max_tokens_to_sample: 200,  
        stop_sequences: [],  
        contentType: 'application/json',  
        accept: '*/*',  
        top_p: 0.9, // 常用的核采样,此参数影响生成文本的多样性。  
        temperature: 0.5, // 值为0.5表示中等程度的随机性,允许响应有一定的变化性,但仍优先考虑一致性。  
        top_k: 5, // 该参数限制模型仅从最有可能的下一个标记中采样  
      };  

      const modelResponse: ModelResponses = await invokeBedrockApi(  
        params,  
        modelId,  
        bedrockVersion,  
      );  

      logger.debug(  
        `Bedrock响应报告: ${modelResponse[0].text} for id ${id}`,  
      );  

      // 返回Bedrock生成的摘要和原始结果信息
      return { summary: modelResponse[0].text, result: motResult };  
    }

从上面的代码我们可以看出,它首先根据已部署的阶段(develop|test|staging|prod)获取正确的Garage API来调用;在测试阶段,我们使用API Test Harness API,而在其他阶段,我们使用实际的Garage API实现。具体如下所示:

    import { getParameter } from '@aws-lambda-powertools/parameters/ssm';  
    import { logger } from '@shared';  
    import { Stage } from '../../../types';  

    // 如果阶段是测试阶段,使用 API 测试工具
    export async function getRestEndpoint(stage: Stage): Promise<string> {  
      const restEndpoint =  
        stage === Stage.test  
          ? ((await getParameter(`/${stage}/api-test-工具-url`)) as string)  
          : ((await getParameter(`/${stage}/正式API地址`)) as string);  

      logger.info(`记录使用的 API 地址: ${restEndpoint}`);  

      return restEndpoint;  
    }

回到上面提到的 Lambda 函数,一旦它获得了正确的 REST API 用于调用,它会检查对于给定的客户 ID 的通信偏好。

    // 注意:为了演示方便,我们为每个客户硬编码了偏好值。  
    // 但是在实际应用中,这些偏好信息通常会存储在数据库表中。  
    export async function getCustomerCommunicationPreferences(  
      customerId: string,  
    ): Promise<string> {  
      let communicationPreference;  
      switch (customerId) {  
        case '1':  
          communicationPreference = '要点';  
          break;  
        case '2':  
          communicationPreference = '简短摘要';  
          break;  
        case '3':  
          communicationPreference = '详细说明';  
          break;  
        case '4':  
          communicationPreference = '问答';  
          break;  
        default:  
          communicationPreference = '简短摘要';  
          break;  
      }  
      return communicationPreference;  
    }

再次强调,为了展示响应的多变性,我们确保每次使用不同的客户ID调用客户API时,都会得到不同类型的生成摘要结果;从项目符号到问答形式!

💡 注意:这在演示中是硬编码的,而实际上,每个客户的沟通方式更可能被存储在数据库中。

行了,再来看看这段Lambda函数代码,该代码现在调用我们的API(无论是测试版API还是实际API)来获取车辆检查结果。

发起一个 HTTP GET 请求到 `v1/results/${id}`,并将响应结果转换为 `Result` 类型。

我们现在来用AI根据检查数据生成摘要,这很有趣:

    const params: ActionPromptDto = {  
        prompt: `Human:请总结一份给非技术客户的汽车检测结果,该客户喜欢${communicationPreference}  
                 其中检测结果详情为${JSON.stringify(motResult)},总结应包含  
                 车辆颜色、车型、车系、检测日期、到期日、测试中心名称和地址,以及任何建议。  
                 请给出结果时不要提及这是为非技术客户准备的。Assistant:`,  
        max_tokens_to_sample: 200,  
        stop_sequences: [],  
        contentType: 'application/json',  
        accept: '*/*',  
        top_p: 0.9, // 也称为核采样,此参数影响生成文本的多样性。  
        temperature: 0.5, // 值为0.5表示中等水平的随机性,允许对响应进行一些调整,同时优先考虑连贯。  
        top_k: 5, // 此参数限制模型仅从最有可能的前k个标记中采样  
      };  

      const modelResponse: ModelResponses = await invokeBedrockApi(  
        params,  
        modelId,  
        bedrockVersion,  
      );  

      // 响应展示了Bedrock生成的总结和原始结果  
      return { summary: modelResponse[0].text, result: motResult };

我们这样设计提示:

  • 用简单易懂的语言向非技术人员客户总结报告内容。
  • 报告需要包含一些特定信息,比如检查日期和汽车的品牌和型号。
  • 根据客户的沟通偏好返回相应的回复。

回复客户的例子,如下:

    {  
        "summary": "以下是车辆检测的结果:\n\n这辆2023年的克莱斯勒 Charger 车辆为蓝色。检测日期为2024年10月17日,证书有效期至2025年8月1日。此次检测在位于北特萨博罗,奎格利群岛 57566 的金-道格拉斯测试中心进行。\n\n车辆通过了检测,但仍有一些需要注意的地方:刹车片磨损严重,以及大灯照射角度调得过高。此外,还有一个建议注意点:靠近车辆前方的左侧前轮胎磨损接近法定限值。\n\n",  
        "result": {  
            "id": "1",  
            "created": "2024-11-04T10:02:39.987Z",  
            "updated": "2024-11-04T10:02:39.987Z",  
            "vehicle": {  
                "registration": "AG16ELO",  
                "make": "克莱斯勒",  
                "model": "Charger",  
                "color": "蓝色",  
                "yearOfManufacture": 2023  
            },  
            "motResult": {  
                "testDate": "2024-10-17",  
                "expiryDate": "2025-08-01",  
                "result": "PASS",  
                "testCenter": {  
                    "name": "金-道格拉斯",  
                    "location": {  
                        "addressLine1": "57566 奎格利群岛",  
                        "town": "北特萨博罗",  
                        "postcode": "67590"  
                    }  
                },  
                "mileage": 63649,  
                "defects": [  
                    {  
                        "code": "D001",  
                        "description": "刹车片磨损严重"  
                    },  
                    {  
                        "code": "D002",  
                        "description": "大灯照射角度调得过高"  
                    }  
                ],  
                "advisories": [  
                    {  
                        "code": "A001",  
                        "description": "靠近车辆前方的左侧前轮胎磨损接近法定限值"  
                    }  
                ]  
            }  
        }  
    }

生成的 JSON 部分如下所示(每次收到不同的 JSON 数据时都会有所不同):

“该车辆是一辆2023年的克莱斯勒公羊,颜色为蔚蓝色。检查日期为2024年10月17日,证书有效期至2025年8月1日。该检查在位于北特雷斯阿博罗镇57566号奎格利岛的金-道格拉斯检测中心进行。

车辆通过了检查,但是报告指出存在两个缺陷:刹车片磨损低于最低厚度要求,前大灯的高度不合适。此外,还有一个建议注意点:靠近车身左侧前轮的磨损接近磨损极限。”

现在我们知道了这些服务是如何协同工作的,让我们来介绍一下测试环境,这个环境会测试上面的例子,检查其置信度分数、语调,以及它是否通过了我们对它的所有验证!

测试环境

好的,我们现在来看一下测试套件中的一个测试,看看它是什么样的:

    import {  
      clearTable,  
      generateRandomId,  
      getParameter,  
      httpCall,  
      putItem,  
    } from '@packages/aws-async-test-library';  
    import {  
      AssertionsMet,  
      Tone,  
      responseAssertions,  
    } from '@packages/aws-async-test-library/ai-assertions';  

    // 常量  
    let customerEndpoint: string;  
    const testHarnessTable = `api-test-harness-table-test`;  

    describe('api-responses-journey', () => {  
      beforeAll(async () => {  
        // 从 SSM 获取客户端点,用于我们的 API 测试套件  
        customerEndpoint = await getParameter(`/test/customer-api-url`);  
        await clearTable(testHarnessTable, 'pk', 'sk');  
      }, 12000);  

      beforeEach(async () => {  
        await clearTable(testHarnessTable, 'pk', 'sk');  
      }, 12000);  

      afterAll(async () => {  
        await clearTable(testHarnessTable, 'pk', 'sk');  
      }, 12000);  

      describe('验证', () => {  
        it('应该以 8 分或以上的置信度分数通过验证', async () => {  
          expect.assertions(3);  

          // 安排 - 1. 设置我们的 API 测试套件响应,从内部车库 API 获取,并生成一个确定的 API 响应,该响应将传递给车库服务中的 Bedrock  
          const testId = generateRandomId();  

          await putItem(testHarnessTable, {  
            pk: testId,  
            sk: 1,  
            statusCode: 200,  
            response: {  
              id: '1',  
              created: '2024-11-02T14:38:06.004Z',  
              updated: '2024-11-02T14:38:06.004Z',  
              vehicle: {  
                registration: 'RI56DMZ',  
                make: 'NIO',  
                model: 'XC90',  
                color: '象牙色',  
                yearOfManufacture: 2019,  
              },  
              motResult: {  
                testDate: '2024-04-04',  
                expiryDate: '2025-03-25',  
                result: 'FAIL',  
                testCenter: {  
                  name: 'Harvey and Nader',  
                  location: {  
                    addressLine1: '8902 Paris Mountains',  
                    town: 'Savannaworth',  
                    postcode: '18488-6552',  
                  },  
                },  
                mileage: 58567,  
                defects: [],  
                advisories: [  
                  {  
                    code: 'A002',  
                    description: '破裂的转向横拉杆内关节',  
                  },  
                ],  
              },  
            },  
          });  

          // 执行 - 调用外部的客户 API(在测试阶段使用我们 API 测试套件中的服务)  
          const response = await httpCall(  
            customerEndpoint,  
            `v1/customers/3/results/1`,  
            'GET',  
          );  

          // 断言 - 使用一组断言测试摘要响应是否正确  
          const assertionPrompt = `  
          - 它指出车辆尽管有建议事项,仍然未通过测试。  
          - 它说明了测试中心的名称以及测试地点的地址。  
          - 它详细说明了 2024 年 4 月 4 日的测试日期和 2025 年 3 月 25 日的过期日期。  
          - 它说明车辆品牌为 NIO,型号为 XC90,颜色为象牙色。  
          - 它说明有一个建议事项,并给出了详细信息。`;  

          const assertionResponse = await responseAssertions({  
            prompt: assertionPrompt,  
            text: response.summary,  
          });  

          expect(assertionResponse.assertionsMet).toEqual(AssertionsMet.yes); // 它通过了提供的断言  
          expect(assertionResponse.tone).toEqual(Tone.neutral);  
          expect(assertionResponse.score).toBeGreaterThanOrEqual(8); // 置信度分数应大于或等于8  
        }, 120000);  
      });  
    });

从上面的 Jest 测试套件中,我们可以看到,我们首先从 AWS 参数存储库获取公开的客户 REST API:

    ...
    // 常量
    let customerEndpoint: string;
    const testHarnessTable = `api-test-harness-table-test`;

    describe('api-responses-journey', () => {
      在所有测试前(async () => {
        // 我们从SSM获取客户端点,以便进行API测试
        customerEndpoint = await getParameter(`/test/customer-api-url`);
        await clearTable(testHarnessTable, 'pk', 'sk');
      }, 12000);

      在每次测试前(async () => {
        await clearTable(testHarnessTable, 'pk', 'sk');
      }, 12000);

      在所有测试后(async () => {
        await clearTable(testHarnessTable, 'pk', 'sk');
      }, 12000);
    ...

我们会在所有测试开始前、每个测试开始前以及所有测试结束后清理API 测试工具的数据库。我们通过以下文件夹中的几个辅助函数来完成这项工作,这使我们的e2e测试每次运行时都保持干净。

客户API/包/aws-async-test-library

这些是围绕AWS SDK构建的基本可重用包装器,它们帮助我们在测试中。我们现在开始运行第一个端到端测试。

     it('应验证置信度分数为8分或更高', async () => {  
          expect.assertions(3);  

          // 安排 - 1. 设置来自内部车库API的API测试模拟响应  
          // 并创建一个确定性的API响应,该响应将传递给车库服务中的Bedrock  
          const testId = generateRandomId();  

          await putItem(testHarnessTable, {  
            pk: testId,  
            sk: 1,  
            statusCode: 200,  
            response: {  
              id: '1',  
              created: '2024-11-02T14:38:06.004Z',  
              updated: '2024-11-02T14:38:06.004Z',  
              vehicle: {  
                registration: 'RI56DMZ',  
                make: 'NIO',  
                model: 'XC90',  
                color: '象牙白',  
                yearOfManufacture: 2019,  
              },  
              motResult: {  
                testDate: '2024-04-04',  
                expiryDate: '2025-03-25',  
                result: 'FAIL',  
                testCenter: {  
                  name: 'Harvey and Nader',  
                  location: {  
                    addressLine1: '8902 巴黎山脉',  
                    town: 'Savannaworth',  
                    postcode: '18488-6552',  
                  },  
                },  
                mileage: 58567,  
                defects: [],  
                advisories: [  
                  {  
                    code: 'A002',  
                    description: '内关节破损',  
                  },  
                ],  
              },  
            },  
          });  

          // 执行 - 我们调用外部的客户API  
          const response = await httpCall(  
            customerEndpoint,  
            `v1/customers/3/results/1`,  
            'GET',  
          );  

          // 断言 - 我们通过一系列断言测试总结响应是否正确  
          const assertionPrompt = `  
          - 指出车辆未通过检查,尽管有建议。  
          - 指出了测试中心名称和检查地点的地址。  
          - 详细说明了2024年4月4日的检查日期和2025年3月25日的过期日期。  
          - 指出车辆品牌为NIO,型号为XC90,颜色为象牙白。  
          - 其中包括一个建议,并提供了该建议的详细信息。`;  

          const assertionResponse = await responseAssertions({  
            prompt: assertionPrompt,  
            text: response.summary,  
          });  

          expect(assertionResponse.assertionsMet).toEqual(AssertionsMet.yes); // 满足提供的断言  
          expect(assertionResponse.tone).toEqual(Tone.neutral);  
          expect(assertionResponse.score).toBeGreaterThanOrEqual(8); // 置信度分数应等于或高于8  
        }, 120000);

我们可以看到,我们为API测试工具数据库填充初始数据(即,这将返回测试数据而不是通过调用车库API获取)。这在以下文章中详细说明了:https://medium.com/deterministic-api-test-harness-for-aws-step-function-e2e-tests-8cb641c01674

    ...  
    await putItem(testHarnessTable, {  
      pk: testId,  
      sk: 1,  
      statusCode: 200,  
      response: {  
        id: '1',  
        created: '2024-11-02T14:38:06.004Z',  
        updated: '2024-11-02T14:38:06.004Z',  
        vehicle: {  
          registration: 'RI56DMZ',  
          make: 'NIO',  
          model: 'XC90',  
          color: '象牙色',  
          yearOfManufacture: 2019,  
        },  
        motResult: {  
          testDate: '2024-04-04',  
          expiryDate: '2025-03-25',  
          result: 'FAIL',  
          testCenter: {  
            name: 'Harvey and Nader',  
            location: {  
              addressLine1: '8902 巴黎山',  
              town: '萨凡沃斯',  
              postcode: '18488-6552',  
            },  
          },  
          mileage: 58567,  
          defects: [],  
          advisories: [  
            {  
              code: 'A002',  
              description: '损坏的转向横拉杆内连接',  
            },  
          ],  
        },  
      },  
    });  
    ...

这意味着上述数据将被输入到客户服务中心里的 Lambda 函数中的 AI模型,因此可以预测生成的摘要。

测试接着设置一些断言,来检查它是否符合响应摘要中的预期内容,例如:

✔️ 它表明无论是否有建议,车辆仍未能通过检验。
✔️ 它列出了检验中心的名字和地址。
✔️ 它详细说明了检验日期为2024年4月4日,截至日期为2025年3月25日。
✔️ 它指明车辆制造商为NIO,型号为XC90,颜色为象牙白。
✔️ 它提到一条建议并详细说明了建议的内容。

如下代码所示:

    // 断言 - 我们通过一组断言测试总结响应的正确性  
    const 断言提示 = `  
    - 它说明无论是否有建议,车辆都未通过检验。  
    - 它说明了检验中心的名称及检验地点的地址。  
    - 它详细说明了检验日期为2024年4月4日,有效期到2025年3月25日。  
    - 它说明车辆品牌为NIO,型号为XC90,颜色为象牙色。  
    - 它说明有一个建议,并详细说明了该建议的内容。`;  

    const 断言响应 = await responseAssertions({  
      prompt: 断言提示,  
      text: response.summary,  
    });  

    expect(断言响应.assertionsMet).toEqual(AssertionsMet.yes); // 它通过了断言  
    expect(断言响应.tone).toEqual(Tone.中立);  
    expect(断言响应.score).toBeGreaterThanOrEqual(8); // 置信度较高  
    } 120000);

断言接着传递给 responseAssertions 测试框架函数,连同我们要测试的由 AI 生成的摘要,以获取可以进行断言的结果。响应的格式如下所示:

{
  "assertionsMet": boolean,
  "score": number,
  "tone": string,
  "explanation": string
}

这使我们能够使用Jest运行一组预期的结果,如下所示:

    expect(assertionResponse.assertionsMet).toEqual(AssertionsMet.yes); // 确保通过了提供的断言  
    expect(assertionResponse.tone).toEqual(Tone.neutral); // 确保语气是中立的  
    expect(assertionResponse.score).toBeGreaterThanOrEqual(8); // 确保有较高的置信度分数

现在我们快速看一下这个可重用框架中的测试断言函数。

测试断言 (Test Assertions)

如上所示的代码,我们使用了名为 responseAssertions 的函数,该函数属于我们使用的 aws-async-test-library 包,它允许我们传递一些断言来验证一段文本内容,以测试文本内容。

代码如下:

[需要插入实际代码]

Note: The placeholder "[需要插入实际代码]" still needs to be replaced with the actual code snippet as per the expert suggestions.

    import {  
      BedrockRuntimeClient,  
      InvokeModelCommand,  
      InvokeModelCommandInput,  
    } from '@aws-sdk/client-bedrock-runtime';  

    export type ActionPromptDto = {  
      contentType: string;  
      accept: string;  
      prompt: string;  
      max_tokens_to_sample: number;  
      stop_sequences: string[];  
      temperature: number;  
      top_p: number;  
      top_k: number;  
    };  

    export type ModelResponse = {  
      type: string;  
      text: string;  
    };  

    export type ModelResponses = ModelResponse[];  

    export enum Tone {  
      neutral = 'neutral',  
      happy = 'happy',  
      sad = 'sad',  
      angry = 'angry',  
    }  

    export const AssertionsMet = {  
      yes: true,  
      no: false,  
    };  

    export type AssertionResponse = {  
      assertionsMet: typeof AssertionsMet;  
      score: number;  
      tone: Tone;  
      explanation: string;  
    };  

    export interface ResponseAssertionsInput {  
      prompt: string;  
      text: string;  
      modelId?: string;  
      bedrockVersion?: string;  
      maxTokensToSample?: number;  
      topP?: number;  
      topK?: number;  
      contentType?: string;  
      accept?: string;  
      stopSequences?: string[];  
      temperature?: number;  
    }  

    const bedrock = new BedrockRuntimeClient({});  

    const invokeBedrockApi = async (  
      actionPromptDto: ActionPromptDto,  
      modelId: string,  
      bedrockVersion: string,  
    ): Promise<ModelResponses> => {  
      const {  
        accept,  
        contentType,  
        max_tokens_to_sample,  
        prompt,  
        top_p,  
        top_k,  
        temperature,  
      } = actionPromptDto;  

      const body = JSON.stringify({  
        anthropic_version: bedrockVersion,  
        max_tokens: max_tokens_to_sample,  
        temperature: temperature,  
        top_p: top_p,  
        top_k: top_k,  
        messages: [  
          {  
            role: 'user',  
            content: [  
              {  
                type: 'text',  
                text: prompt,  
              },  
            ],  
          },  
        ],  
      });  

      const input: InvokeModelCommandInput = {  
        body,  
        contentType,  
        accept,  
        modelId,  
      };  

      const params = new InvokeModelCommand(input);  

      try {  
        const { body: promptResponse } = await bedrock.send(params);  

        const promptResponseJson = JSON.parse(  
          new TextDecoder().decode(promptResponse),  
        );  

        const result = promptResponseJson.content;  
        return result;  
      } catch (error) {  
        throw error;  
      }  
    };  

    export async function responseAssertions({  
      prompt,  
      text,  
      modelId = 'anthropic.claude-3-haiku-20240307-v1:0',  
      bedrockVersion = 'bedrock-2023-05-31',  
      maxTokensToSample = 500,  
      topP = 0.7,  
      topK = 200,  
      contentType = 'application/json',  
      accept = 'application/json',  
      stopSequences = [],  
      temperature = 0.3,  
    }: ResponseAssertionsInput): Promise<AssertionResponse> {  
      const assertionPrompt = `分析以下文本和断言,返回:

1. 单一的整体置信度评分(0-10),其中:

* 10 = 所有断言完全匹配

* 7-9 = 大多数断言强匹配

* 4-6 = 一些断言部分匹配

* 0-3 = 很少或没有断言匹配

2. 单一的布尔值 'assertionsMet',只有当所有要测试的断言在文本中完全满足时,它才为真;

* 指定情况下的大小写敏感性

* 部分匹配应该被标记

3. 清晰的解释,涵盖每个断言及其值的评分理由,作为一个字符串值。

4. 基于以下内容的情感基调(中立、愤怒、快乐或悲伤):
       - 情感词汇和短语
       - 标点符号和强调
       - 整体上下文

    返回单一的JSON对象结果。

    返回的JSON应包含:
        {  
            "assertionsMet": 布尔值,  
            "score": 数值,  
            "tone": 字符串,  
            "explanation": 字符串  
        }

    文本:

    "${text}"  

    断言:

     ${prompt}  

    .助手:`;  

      const result: ModelResponses = await invokeBedrockApi(  
        {  
          contentType,  
          accept,  
          prompt: assertionPrompt,  
          max_tokens_to_sample: maxTokensToSample,  
          stop_sequences: stopSequences,  
          temperature,  
          top_p: topP,  
          top_k: topK,  
        },  
        modelId,  
        bedrockVersion,  
      );  

      return JSON.parse(result[0].text.replace(/[\x00-\x1F\x7F]/g, ''));  
    }

从上面的代码中可以看出,我们为基本模型设置了合理的默认参数,但允许用户覆盖调用的任何部分。然后我们将这两个参数插入到发送给Amazon Bedrock的提示中,1. 断言提示,2. 需要验证的文本摘要。这意味着我们使用AI来验证Customer API生成的AI响应。

💡 注意:如果我要真正发布这个框架供他人使用,我会换成相反的API接口以提供更大的灵活性,但这只是为了展示可能的范围。

我们的提示验证在返回 JSON 格式响应前会验证以下内容,让我们的 Jest 测试能使用。

1. 一个总体置信分数(0-10),其中:  

* 10 = 所有断言完美匹配  

* 7-9 = 大多数断言强烈匹配  

* 4-6 = 一些断言部分匹配  

* 0-3 = 很少或没有断言匹配  

2. 一个单一的布尔值 'assertionsMet',只有当文本中的所有断言都被完全满足时才为真。  

* 大小写敏感的地方要严格区分  

* 部分匹配需要被标记  

3. 解释每个断言及其对应的评分的一个字符串值,涵盖每个断言的评分依据。  

4. 根据以下内容确定情感基调(中立、愤怒、快乐、悲伤):  
   - 情感词汇和短语等  
   - 标点符号和强调  
   - 整体上下文  

返回结果为一个单一的 JSON 对象。  

返回 JSON 包含:  
    {  
        "assertionsMet": boolean,  
        "score": number,  
        "tone": string,  
        "explanation": string  
    }
最后,让我们来看看结论。

感谢您阅读这篇文章,最后再简单回顾一下:

✔️ 我们讨论了测试我们AI提示输出的重要性。
✔️ 我们讨论了断言(assertions)、置信度评分和评估。
✔️ 我们讨论了确定性API响应的重要性。
✔️ 我们深入探讨了AWS CDK和TypeScript代码示例。

结束一下👋

希望你喜欢这篇短文,如果你喜欢,别忘了分享哦,也欢迎你留下宝贵意见!

请在YouTube上订阅我的频道 https://www.youtube.com/channel/UC_Bi6eLsBXpLnNRNnxKQUsA 查看类似内容!感谢支持。

我也很希望能通过以下任何一种方式与您联系:

这是我的LinkedIn个人资料页面:https://www.linkedin.com/in/lee-james-gilmore/。这是我的Twitter页面:https://twitter.com/LeeJamesGilmore

如果你喜欢这些帖子,请关注我 李詹姆斯吉尔莫,以获取更多帖子和系列,并别忘了联系并打声招呼 👋

如果你喜欢这篇帖子,请在帖子底部使用‘点赞’功能!(你可以连续点赞哦!)

我来说

“大家好,我是李,AWS Serverless 英雄之一、博客作者、AWS 认证的云架构师,同时也是英国的首席云架构师和云实践负责人;过去的十年里,我主要在 AWS 上做全栈 JavaScript 开发。”

我认为自己是一名无服务器的倡导者,热爱AWS、创新、软件架构和技术等领域。

以下提供的信息是我的个人观点,对于信息的使用后果我不承担责任。

你也可能会对以下感兴趣哦:

无服务器相关内容 🚀 我的所有无服务器相关内容的索引,方便在这里轻松查看,包括视频、博客文章等..https://blog.serverlessadvocate.com
点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消