我们的 agent 技能如何调研、构建和测试完整的 Elastic 集成

0 阅读14分钟

作者:来自 Elastic Chris Berkhout

我们让 agent 技能处理美国国家标准与技术研究院的国家漏洞数据库接口,得到了两个数据流和一个经过验证的通用表达式语言程序,并通过了流水线测试和系统测试。调研发现,一天中 78% 的通用漏洞与披露记录都是旧记录的重新发布,这一发现改变了设计。

Elasticsearch 原生集成了业界领先的生成式 人工智能 工具和服务提供商。欢迎观看我们的网络研讨会,了解如何超越检索增强生成基础( Beyond RAG Basics ),或使用 Elastic 向量数据库( Elastic Vector Database )构建可用于生产环境的应用。

要为你的使用场景构建最佳搜索解决方案,立即开始免费云试用( free cloud trial ),或在你的本地计算机( local machine )上试用 Elastic。


Agent 现在可以端到端构建 Elastic 集成。Elastic 集成技能( Elastic Integration Skills )将 Elastic 在调研、软件包骨架搭建、通用表达式语言开发、摄取流水线、测试和审核方面的最佳实践编码为技能。它们将工作拆分为多个小单元,每个单元都会产出经过验证的结果,最终组合成完整、可运行的集成。

我们让这些技能处理美国国家标准与技术研究院国家漏洞数据库( NIST National Vulnerability Database )接口,得到了两个数据流( data streams ),配有经过验证的通用表达式语言程序,并通过了流水线测试和系统测试。该软件包在几分钟内回填了一周的数据,收集到 7,408 份通用漏洞与披露记录快照和 12,339 条变更事件。

此前的一篇文章( A previous post )介绍通用表达式语言输入时,假定你需要自行编写程序。现在已不再需要这样做,你可以专注于流程的其他部分。你决定构建什么,再判断证据是否充分、结果是否值得信赖。

我们的 agent 技能如何在专业 agent 之间分配工作

完整集成通过编排专业 agent 的工作来构建。一个编排 agent 会话会为每个开发阶段派遣子 agent:调研、通用表达式语言程序、摄取流水线、系统测试和审核。每个子 agent 都会先阅读技能中的指令,再开始操作。编排 agent 将大部分工作委派给子 agent,向它们传递上下文,并读取返回的报告。

你与编排 agent 协作,为子 agent 提供上下文。在它们工作时,你无需跟进生成通用表达式语言代码等具体步骤,而是会收到代码已通过验证的报告。然后,你再决定检查输出文件中的哪些内容。

为获得最佳效果,请使用顶级 推理模型 ,例如 Claude Opus 或 GPT-6 Astra。按照说明( instructions )安装所有依赖。请保持集成技能为最新版本,因为它们会持续改进。

从接口调研开始

有两个顶层技能:/research-integration 和 /create-integration。在开始时进行全面调研,有助于后续顺利完成创建。调研技能会研究接口文档及其实际行为,收集样本数据,并将发现整理成供创建阶段使用的调研简报。要塑造最终成果,最有效的方式就是迭代完善调研输出。

一条好的调研 提示词 应简要表达你的意图,并引用与该集成密切相关的资源:

只要输入组件持续运行,每次求值的输出就会作为下一次求值的输入继续传递。键 cursor 下的输出数据会持久化保存到磁盘,并在输入组件重启后重新加载,但其余状态不会在重启后保留。

通用表达式语言本身功能有限,并且避免副作用,但它可以扩展。cel-go 实现增加了一些功能,例如可选语法和类型。Mito 库在 cel-go 的基础上进一步扩展了功能,包括发送超文本传输协议请求的能力。通用表达式语言输入组件使用的是 Mito 版本的通用表达式语言。

使用 Mito

要使用通用表达式语言输入组件构建或调试集成,最重要的是理解:对于给定的输入状态,你的通用表达式语言程序会产生什么输出状态。开发过程中,让输入组件在完整的 Elastic 技术栈中运行程序,可能会比较繁琐。要获得更快的反馈,一种方法是使用 Mito 的命令行工具,直接运行通用表达式语言程序,查看它在给定输入下产生的输出。

Mito 使用 Go 语言编写,可以通过以下方式安装:

`go install github.com/elastic/mito/cmd/mito@latest`AI代码解释

使用 Mito 运行通用表达式语言程序时,通常需要提供两个文件:一个包含初始输入状态的 JSON 文件,以及另一个包含程序源代码的文件:

`mito -data state.json src.cel`AI代码解释

为方便复制粘贴,本文中的示例都写成单条命令,通过将每个文件的内容包裹在 <(echo '...content...') 中,让命令行解释器即时创建临时文件。在你自己的开发过程中,使用实际文件会更方便。

从 GitHub 获取议题数据

以下示例包含一个完整的通用表达式语言程序,用于从 GitHub 接口( GitHub API )获取议题数据。其初始输入状态包含接口端点地址,以及有关如何处理分页的信息。程序使用输入状态中的数据生成请求,然后解码响应,从中生成事件,并将这些事件作为输出状态的一部分返回。

`

1.  mito -data <(echo '
2.    {
3.      "url": "https://api.github.com/repos/elastic/integrations/issues",
4.      "per_page": 3,
5.      "max_pages": 3
6.    }
7.  ') <(echo '
8.    int(state.?cursor.page.orValue(1)).as(page,
9.      (
10.        state.url + "?" + {
11.          "state": ["all"],
12.          "sort": ["created"],
13.          "direction": ["asc"],
14.          "per_page": [string(state.per_page)],
15.          "page": [string(page)],
16.        }.format_query()
17.      ).as(full_url,
18.        request("GET", full_url).with({
19.          "Header": {
20.            "Accept": ["application/vnd.github+json"],
21.            "X-GitHub-Api-Version": ["2022-11-28"],
22.          }
23.        }).do_request().as(resp,
24.          resp.Body.decode_json().as(data,
25.            state.with({
26.              "events": data.map(i, {
27.                "html_url": i.html_url,
28.                "title": i.title,
29.                "created_at": i.created_at,
30.              }),
31.              "cursor": { "page": page + 1 },
32.              "want_more": size(data) == state.per_page && page < state.max_pages,
33.            })
34.          )
35.        )
36.      )
37.    )
38.  ')

`AI代码解释![](https://csdnimg.cn/release/blogv2/dist/pc/img/runCode/icon-arrowwhite.png)

首次求值会产生以下输出:

`

1.  {
2.    "cursor": {
3.      "page": 2
4.    },
5.    "events": [
6.      {
7.        "created_at": "2018-09-14T09:47:35Z",
8.        "html_url": "https://github.com/elastic/integrations/issues/3250",
9.        "title": "Increase support of log formats in haproxy filebeat module"
10.      },
11.      {
12.        "created_at": "2019-02-06T12:37:37Z",
13.        "html_url": "https://github.com/elastic/integrations/issues/487",
14.        "title": "ETCD Metricbeat module needs polishing and grooming"
15.      },
16.      {
17.        "created_at": "2019-08-13T11:33:11Z",
18.        "html_url": "https://github.com/elastic/integrations/pull/1",
19.        "title": "Initial structure"
20.      }
21.    ],
22.    "max_pages": 3,
23.    "per_page": 3,
24.    "url": "https://api.github.com/repos/elastic/integrations/issues",
25.    "want_more": true
26.  }

`AI代码解释![](https://csdnimg.cn/release/blogv2/dist/pc/img/runCode/icon-arrowwhite.png)

事件会被移除;在通用表达式语言输入组件中运行时,这些事件会被发布以供摄取。其余输出将作为输入状态,传递给下一次通用表达式语言程序求值。

为了理解这个通用表达式语言程序的工作原理,我们将查看一些更简短的示例,并进一步介绍输入组件的运行细节。

通用表达式语言基础

在通用表达式语言中,没有语句,只有表达式。每个成功求值的表达式都会得到一个最终值。下面是你能编写的最简短的通用表达式语言表达式之一,以及它的输出:

`

1.  mito <(echo '
2.    "hello" + " " + "world"
3.  ')

`AI代码解释
`"hello world"`AI代码解释

许多简单表达式都很直观。数学运算仅支持相同类型的值(例如,int 与 int),因此需要按需转换类型(此处从 int 转换为 double):

`

1.  mito <(echo '
2.    double((1 + 2) * (3 + 4)) / 2.0
3.  ')

`AI代码解释
`10.5`AI代码解释

通用表达式语言中没有变量,但借助 Mito 的 as 宏,可以为表达式的结果命名,并在更大的表达式中使用它。在这个示例中,表达式 (1 + 1) 求值得到 2,而 .as(n, ...) 将该值命名为 n,供表达式 "one plus one is "+string(n) 使用,其含义为“一加一等于”加上 n 的字符串形式:

`

1.  mito <(echo '
2.    (1 + 1).as(n, "one plus one is "+string(n))
3.  ')

`AI代码解释
`"one plus one is 2"`AI代码解释

也可以将信息累积到映射中,并在表达式的后续部分使用,如下例通过 with 所示:

`

1.  mito <(echo '
2.    { "key": "value" }.with({ "key2": "value2" }).as(data,
3.      {
4.        "data": data,
5.        "size": size(data),
6.      }
7.    )
8.  ')

`AI代码解释
`

1.  {
2.    "data": {
3.      "key": "value",
4.      "key2": "value2"
5.    },
6.    "size": 2
7.  }

`AI代码解释

再看一下这个示例。注意,嵌套部分 ({ "data": data, "size": size(data), }) 展示了最终值的结构。它是一个包含键 "data" 和 "size" 的映射。这些键的值取决于 data,而 data 由表达式的外层部分定义。从内向外阅读通用表达式语言表达式,有助于快速看出它们会返回什么。

通用表达式语言没有 if 这样的控制流语句,但可以使用三元运算符实现条件分支:

`

1.  mito <(echo '
2.    1 + 1 < 12 ? "few" : "many"
3.  ')

`AI代码解释
`"few"`AI代码解释

通用表达式语言不是图灵完备的语言,因此不支持无界循环和递归。这使得执行时间可预测,并且与输入数据的大小和表达式的复杂度成正比。

尽管单个通用表达式语言表达式无法执行无界循环,但你可以使用 map 等宏来处理列表和映射:

`

1.  mito <(echo '
2.    [1, 2, 3].map(x, x * 2)
3.  ')

`AI代码解释
`[2, 4, 6]`AI代码解释

本节介绍了:

  • 字符串、数字、列表和映射。
  • 字符串拼接。
  • 数学运算。
  • 类型转换。
  • 条件表达式。
  • 为子表达式命名。
  • 处理集合。

接下来,我们将介绍如何发送超文本传输协议请求。

请求

Mito 扩展了通用表达式语言,使其能够发送超文本传输协议请求( HTTP requests ):

`

1.  mito <(echo '
2.    get("https://example.com").as(resp, string(resp.Body))
3.  ')

`AI代码解释
`"<!doctype html><html lang=\"en\"><head><title>Example Domain</title>..."`AI代码解释

请求可以在执行前显式构建。这样就能使用不同的超文本传输协议方法,并添加请求头和请求体。

在这个示例中,我们借助 format_query 构建地址,为请求添加一个请求头,并使用 decode_json 解析响应体。指定 -log_requests 选项后,Mito 会以 JSON 格式记录每次请求和响应的详细信息。

`

1.  mito -log_requests <(echo '
2.    request("GET",
3.      "https://postman-echo.com/get?" + {
4.          "q": ["query value"]
5.       }.format_query()
6.    ).with({
7.      "Header": { "Accept": ["application/json"] }
8.    }).do_request().as(resp, {
9.      "status": resp.StatusCode,
10.      "data": resp.Body.decode_json(),
11.    })
12.  ')

`AI代码解释![](https://csdnimg.cn/release/blogv2/dist/pc/img/runCode/icon-arrowwhite.png)
`

1.  {"time":"...","level":"INFO","msg":"HTTP request",...}
2.  {"time":"...","level":"INFO","msg":"HTTP response",...}
3.  {
4.    "data": {
5.      "args": {
6.        "q": "query value"
7.      },
8.      "headers": {
9.        "accept": "application/json",
10.        "accept-encoding": "gzip, br",
11.        "host": "postman-echo.com",
12.        "user-agent": "Go-http-client/2.0",
13.        "x-forwarded-proto": "https"
14.      },
15.      "url": "https://postman-echo.com/get?q=query+value"
16.    },
17.    "status": 200
18.  }

`AI代码解释![](https://csdnimg.cn/release/blogv2/dist/pc/img/runCode/icon-arrowwhite.png)

管理状态与求值

我们已经介绍了如何发送请求,以及生成所需输出状态必备的通用表达式语言基础知识。现在,让我们进一步了解输出状态中应该包含什么,以及如何利用它指导后续处理。

集成中的通用表达式语言程序需要确保,其输出状态适合作为下一次求值的输入。配置会设置初始状态,输出中应保留这些状态,并进行适当修改。一种简单的方法是使用 state.with({ ... }),在保留状态映射的同时覆盖部分值。对于小型程序,常见做法是将整个程序包裹在 state.with() 中,这样就无需在每个生成输出数据的分支(例如成功、错误)中重复编写状态传递逻辑。

如果某些状态值是在求值时初始化的,而不是硬编码在初始输入状态中,程序就需要先检查该值是否已存在,再决定是否设置初始值。对可选语法和类型( optional syntax and types )的支持可以帮助实现这一点。在访问映射中的字段时,在字段名前加上问号,就会变为可选访问:它可能解析出一个值,也可能没有值,但仍可继续进行可选访问,并且在没有值时可以方便地提供默认值:

`

1.  mito -data <(echo '{}') <(echo '
2.    int(state.?counter.orValue(0)).as(counter,
3.      state.with({
4.        "counter": counter + 1,
5.        "want_more": counter + 1 < 3,
6.      })
7.    )
8.  ')

`AI代码解释
`

1.  { "counter": 1, "want_more": true }
2.  { "counter": 2, "want_more": true }
3.  { "counter": 3, "want_more": false }

`AI代码解释

在该示例中,从状态中读取的计数器值会被转换为 int,因为按照 JSON 和 JavaScript 的 Number 类型所确立的约定,所有数字在状态中都会被序列化为浮点数。还需注意,Mito 在这里会遵循 "want_more": true 的设置,但在通用表达式语言输入组件中运行时,只有输出同时包含事件,才会重复求值。

由通用表达式语言输入组件运行的程序,必须在输出映射中返回一个 "events" 键。其值可以是事件映射列表、空列表或单个事件映射。单个事件的形式通常用于错误。输入组件会发布该事件,同时也会记录其值;如果事件设置了 error.message 值,该值还会用于更新集成在 Fleet 中的健康状态。如果程序只生成一个非错误事件,最好将其包裹在列表中。

再看一下前面 GitHub 议题程序的输出:

`

1.  {
2.    "url": "https://api.github.com/repos/elastic/integrations/issues",
3.    "per_page": 3,
4.    "max_pages": 3,
5.    "cursor": {
6.      "page": 2
7.    },
8.    "events": [
9.      { ... },
10.      { ... },
11.      { ... }
12.    ],
13.    "want_more": true
14.  }

`AI代码解释![](https://csdnimg.cn/release/blogv2/dist/pc/img/runCode/icon-arrowwhite.png)

该程序通过以下方式有效管理状态:

  • 在 url、per_page 和 max_pages 中保留初始状态值。
  • 在 cursor.page 中添加需要跨重启持久化保存的状态。
  • 在 events 列表中返回可直接发布的事件。
  • 通过 want_more: true 请求立即重新求值。

现在,你已经了解可选访问、状态管理、通用表达式语言基础和超文本传输协议请求,应该能够读懂完整的 GitHub 议题程序了。试着使用 Mito 运行它,并尝试做一些修改。

回顾与资源

本文介绍了什么是通用表达式语言,以及 Mito 库如何对其进行扩展,以供通用表达式语言输入组件使用。通过一个从 GitHub 接口获取议题信息的示例程序,我们看到了通用表达式语言的灵活性,并逐一讲解了理解该程序所需的细节,包括访问初始状态中的设置、与超文本传输协议接口交互、返回待摄取的事件,以及为后续程序执行管理状态。

要进一步学习并使用通用表达式语言输入组件构建集成,可以探索以下资源:

使用通用表达式语言输入组件构建集成时,最有价值的资源或许是现有 Elastic 集成中的通用表达式语言代码,你可以在 GitHub 上找到:

Elastic 集成代码仓库中的 cel.yml.hbs 文件 —— GitHub( cel.yml.hbs files in the Elastic integrations repository - GitHub )

原文:Common Expression Language: Improve data collection in Elastic with CEL input | Elasticsearch Labs