作者:来自 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

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

mito -data state.json src.cel

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

从 GitHub 获取议题数据

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

mito -data <(echo '
  {
    "url": "https://api.github.com/repos/elastic/integrations/issues",
    "per_page": 3,
    "max_pages": 3
  }
') <(echo '
  int(state.?cursor.page.orValue(1)).as(page,
    (
      state.url + "?" + {
        "state": ["all"],
        "sort": ["created"],
        "direction": ["asc"],
        "per_page": [string(state.per_page)],
        "page": [string(page)],
      }.format_query()
    ).as(full_url,
      request("GET", full_url).with({
        "Header": {
          "Accept": ["application/vnd.github+json"],
          "X-GitHub-Api-Version": ["2022-11-28"],
        }
      }).do_request().as(resp,
        resp.Body.decode_json().as(data,
          state.with({
            "events": data.map(i, {
              "html_url": i.html_url,
              "title": i.title,
              "created_at": i.created_at,
            }),
            "cursor": { "page": page + 1 },
            "want_more": size(data) == state.per_page && page < state.max_pages,
          })
        )
      )
    )
  )
')

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

{
  "cursor": {
    "page": 2
  },
  "events": [
    {
      "created_at": "2018-09-14T09:47:35Z",
      "html_url": "https://github.com/elastic/integrations/issues/3250",
      "title": "Increase support of log formats in haproxy filebeat module"
    },
    {
      "created_at": "2019-02-06T12:37:37Z",
      "html_url": "https://github.com/elastic/integrations/issues/487",
      "title": "ETCD Metricbeat module needs polishing and grooming"
    },
    {
      "created_at": "2019-08-13T11:33:11Z",
      "html_url": "https://github.com/elastic/integrations/pull/1",
      "title": "Initial structure"
    }
  ],
  "max_pages": 3,
  "per_page": 3,
  "url": "https://api.github.com/repos/elastic/integrations/issues",
  "want_more": true
}

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

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

通用表达式语言基础

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

mito <(echo '
  "hello" + " " + "world"
')
"hello world"

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

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

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

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

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

mito <(echo '
  { "key": "value" }.with({ "key2": "value2" }).as(data,
    {
      "data": data,
      "size": size(data),
    }
  )
')
{
  "data": {
    "key": "value",
    "key2": "value2"
  },
  "size": 2
}

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

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

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

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

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

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

本节介绍了:

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

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

请求

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

mito <(echo '
  get("https://example.com").as(resp, string(resp.Body))
')
"<!doctype html><html lang=\"en\"><head><title>Example Domain</title>..."

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

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

mito -log_requests <(echo '
  request("GET",
    "https://postman-echo.com/get?" + {
        "q": ["query value"]
     }.format_query()
  ).with({
    "Header": { "Accept": ["application/json"] }
  }).do_request().as(resp, {
    "status": resp.StatusCode,
    "data": resp.Body.decode_json(),
  })
')
{"time":"...","level":"INFO","msg":"HTTP request",...}
{"time":"...","level":"INFO","msg":"HTTP response",...}
{
  "data": {
    "args": {
      "q": "query value"
    },
    "headers": {
      "accept": "application/json",
      "accept-encoding": "gzip, br",
      "host": "postman-echo.com",
      "user-agent": "Go-http-client/2.0",
      "x-forwarded-proto": "https"
    },
    "url": "https://postman-echo.com/get?q=query+value"
  },
  "status": 200
}

管理状态与求值

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

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

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

mito -data <(echo '{}') <(echo '
  int(state.?counter.orValue(0)).as(counter,
    state.with({
      "counter": counter + 1,
      "want_more": counter + 1 < 3,
    })
  )
')
{ "counter": 1, "want_more": true }
{ "counter": 2, "want_more": true }
{ "counter": 3, "want_more": false }

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

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

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

{
  "url": "https://api.github.com/repos/elastic/integrations/issues",
  "per_page": 3,
  "max_pages": 3,
  "cursor": {
    "page": 2
  },
  "events": [
    { ... },
    { ... },
    { ... }
  ],
  "want_more": true
}

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

  • 在 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

Logo

更多推荐