我们的 agent 技能如何调研、构建和测试完整的 Elastic 集成
作者:来自 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 接口获取议题信息的示例程序,我们看到了通用表达式语言的灵活性,并逐一讲解了理解该程序所需的细节,包括访问初始状态中的设置、与超文本传输协议接口交互、返回待摄取的事件,以及为后续程序执行管理状态。
要进一步学习并使用通用表达式语言输入组件构建集成,可以探索以下资源:
- 通用表达式语言输入组件——Filebeat 文档( CEL input - Filebeat documentation )
- Mito 文档( Mito documentation )
- 通用表达式语言——cel.dev 网站( Common Expression Language - cel.dev website )
- 创建集成——Elastic 文档( Create an Integration - Elastic documentation )
使用通用表达式语言输入组件构建集成时,最有价值的资源或许是现有 Elastic 集成中的通用表达式语言代码,你可以在 GitHub 上找到:
原文:Common Expression Language: Improve data collection in Elastic with CEL input | Elasticsearch Labs
更多推荐
所有评论(0)