我们使用亚马逊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运行测试套件。
让我们在下一节看看我们要建的。
我们在建啥 ⚙️为了亲自展示这个问题,并看看我们如何解决这些问题,以及彻底测试响应,我们来看一下以下的解决方案架构:
从上面的图表中我们可以看到:
- 客户可以使用客户服务REST API来获取他们的MOT车辆检测报告的摘要。
- 客户使用的REST API(API网关)调用一个Lambda函数,该函数调用外部车库的REST API以获取特定客户的检测详情。
- Lambda函数使用从车库API获取的原始JSON响应,使用Amazon Bedrock生成一个面向客户的非技术性报告摘要,并将生成的报告返回给客户。
当我们进行客户API的端到端测试(e2e)时,我们希望确保AI生成的摘要与我们无法控制的车库API响应的一致性,并且我们知道生成的输出每次可能会有所不同。
从上面的e2e测试可以看到:
- 我们使用Jest运行测试套件,它会像预期的那样对面向公众的客户API运行端到端(e2e)测试。
- 我们的Lambda函数现在使用我们的API测试套件,而不是具体的车库REST API实现,这意味着我们现在可以返回测试场景所需的正确车辆检查JSON。
- 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共同学习,写下你的评论
评论加载中...
作者其他优质文章