使用 Discord 和 Elastic Agent Builder A2A 构建游戏社区支持机器人

0 阅读13分钟

作者:来自 Elastic Tomás Murúa

学习如何将 Discord 连接到 Elastic Agent Builder 的 Agent-to-Agent( A2A )服务器,以创建游戏社区支持 bot。

Agent Builder 现已正式发布。通过 Elastic Cloud Trial 开始使用,并在此查看 Agent Builder 的文档。


在这篇文章中,我们将构建一个游戏社区支持机器人,把 Discord 连接到 Elastic Agent BuilderAgent-to-Agent(A2A)服务器。玩家可以提出像 “谁是最强的 Mage( 魔术师)?”或“当前的 meta 是什么?”这样的问题,并通过 Elasticsearch Query Language(ES|QL)分析和语义搜索获得实时答案。

许多游戏公司已经在使用 Elasticsearch 来处理玩家数据和游戏分析。在本教程中,我们将通过 Discord 访问这些数据,而 Discord 正是许多用户日常活跃的平台。

前置条件

我们要构建的内容

我们将创建一个 Discord 机器人,使用 Elastic Agent Builder 的 A2A 服务器来回答玩家问题。这个机器人将有两类工具:

  • 用于分析的 ES|QL 工具:排行榜、英雄统计、meta 报告。
  • 用于知识的 索引搜索 工具:游戏机制、常见问题解答( FAQ )。

架构如下所示:

架构概览

Elastic Agent Builder 提供了一个 A2A 服务器,用于与客户端连接。该服务器暴露了一个 agent,并提供可以使用 ES|QL索引搜索工具来查询 Elasticsearch 的工具。任何兼容 A2A 的客户端都可以连接到它。

A2A 客户端会实例化一个机器人 ,该机器人可以连接到 Discord 服务器,与 Agent Builder 通信,并向用户接收 / 发送消息。

为什么使用 A2A 而不是 MCP ?

来源: developers.googleblog.com/en/a2a-a-ne…

Agent Builder 还提供通过 Model Context Protocol(MCP)服务器暴露 agent 工具的选项。关键区别在于,使用 MCP 时,客户端只能访问工具及其描述,而不能访问整个 agent。此外,工具选择逻辑必须在 MCP 客户端实现,因为所有 agent 的工具会被一起暴露。

使用 A2A 时,整个 agent,包括指令和工具,都可供客户端使用。这让你可以从 Agent Builder 端获得更多控制权,从而集中管理行为,而不是在每个客户端分别管理。

哪种方式更合适取决于你希望控制权存在的位置。对于这个 Discord 机器人,我们希望从 Elastic 管理 agent 的行为,因此 A2A 更适合。

设置示例游戏数据

我们来创建 机器人 可以查询的游戏数据。我们将设置三个索引:

  • player_stats:玩家档案,包括胜场、击杀数、排名。
  • hero_meta:英雄选择率和各段位胜率。
  • game_knowledge:常见问题和游戏机制。使用 semantic_text 进行基于语义而非关键词的匹配。title 和  content 都会复制到 semantic_field,用于混合搜索( hybrid search )解决方案。

创建索引

`

1.  from elasticsearch import Elasticsearch
2.  import os

4.  es = Elasticsearch(
5.      hosts=[os.getenv("ELASTICSEARCH_URL")],
6.      api_key=os.environ["ELASTIC_API_KEY"]
7.  )

9.  # Player stats index
10.  es.indices.create(
11.      index="player_stats",
12.      mappings={
13.          "properties": {
14.              "player_id": {"type": "keyword"},
15.              "username": {"type": "keyword"},
16.              "hero": {"type": "keyword"},
17.              "wins": {"type": "integer"},
18.              "losses": {"type": "integer"},
19.              "kills": {"type": "integer"},
20.              "deaths": {"type": "integer"},
21.              "rank": {"type": "keyword"},
22.              "last_played": {"type": "date"}
23.          }
24.      }
25.  )

27.  # Hero meta index
28.  es.indices.create(
29.      index="hero_meta",
30.      mappings={
31.          "properties": {
32.              "hero_name": {"type": "keyword"},
33.              "pick_rate": {"type": "float"},
34.              "win_rate": {"type": "float"},
35.              "tier": {"type": "keyword"},
36.              "patch_version": {"type": "keyword"}
37.          }
38.      }
39.  )

41.  # Game knowledge index (for semantic search)
42.  es.indices.create(
43.      index="game_knowledge",
44.      mappings={
45.          "properties": {
46.              "title": {"type": "text", "copy_to": "semantic_field"},
47.              "content": {"type": "text", "copy_to": "semantic_field"},
48.              "category": {"type": "keyword"},
49.              "semantic_field": {"type": "semantic_text"} # Semantic search queries this combined field
50.          }
51.      }
52.  )

`AI写代码![](https://csdnimg.cn/release/blogv2/dist/pc/img/runCode/icon-arrowwhite.png)收起代码块![](https://csdnimg.cn/release/blogv2/dist/pc/img/arrowup-line-top-White.png)

索引示例数据

`

1.  from datetime import datetime, timedelta
2.  from elasticsearch.helpers import bulk
3.  import random

5.  # Sample player data
6.  players = [7.      {"player_id": "p001", "username": "DragonSlayer99", "hero": "Warrior", "wins": 342, "losses": 198, "kills": 4521, "deaths": 2103, "rank": "Diamond"},8.      {"player_id": "p002", "username": "ShadowMage", "hero": "Mage", "wins": 567, "losses": 234, "kills": 8932, "deaths": 3421, "rank": "Master"},9.      {"player_id": "p003", "username": "HealBot3000", "hero": "Healer", "wins": 423, "losses": 187, "kills": 1234, "deaths": 1876, "rank": "Diamond"},10.      {"player_id": "p004", "username": "TankMaster", "hero": "Tank", "wins": 298, "losses": 302, "kills": 2341, "deaths": 1543, "rank": "Platinum"},11.      {"player_id": "p005", "username": "AssassinX", "hero": "Assassin", "wins": 789, "losses": 156, "kills": 12453, "deaths": 2987, "rank": "Grandmaster"},12.  ]

14.  for player in players:
15.      player["last_played"] = datetime.now() - timedelta(hours=random.randint(1, 72))

17.  # Hero meta data
18.  heroes = [
19.      {"hero_name": "Warrior", "pick_rate": 15.2, "win_rate": 51.3, "tier": "A", "patch_version": "2.4.1"},
20.      {"hero_name": "Mage", "pick_rate": 22.8, "win_rate": 54.7, "tier": "S", "patch_version": "2.4.1"},
21.      {"hero_name": "Healer", "pick_rate": 18.5, "win_rate": 52.1, "tier": "A", "patch_version": "2.4.1"},
22.      {"hero_name": "Tank", "pick_rate": 12.3, "win_rate": 48.9, "tier": "B", "patch_version": "2.4.1"},
23.      {"hero_name": "Assassin", "pick_rate": 31.2, "win_rate": 49.2, "tier": "A", "patch_version": "2.4.1"},
24.  ]

26.  # Game knowledge for semantic search
27.  knowledge = [28.      {"title": "How to unlock the Dragon Mount", "content": "Complete the Dragon's Lair dungeon on Nightmare difficulty with all party members alive. The mount has a 15% drop rate.", "category": "mounts"},29.      {"title": "Best Mage build for Season 4", "content": "Focus on Intelligence and Critical Chance. Use the Arcane Staff with Frost Runes. Prioritize cooldown reduction for burst damage.", "category": "builds"},30.      {"title": "Understanding the ranking system", "content": "Ranks go from Bronze to Grandmaster. You need 100 points to advance. Wins give 25 points, losses subtract 20.", "category": "ranked"},31.  ]

33.  # Bulk index all data
34.  actions = []
35.  for player in players:
36.      actions.append({"_index": "player_stats", "_source": player})
37.  for hero in heroes:
38.      actions.append({"_index": "hero_meta", "_source": hero})
39.  for doc in knowledge:
40.      actions.append({"_index": "game_knowledge", "_source": doc})

42.  success, errors = bulk(es, actions)
43.  print(f"Indexed {success} documents")

45.  es.indices.refresh(index="player_stats,hero_meta,game_knowledge")

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

完成!现在我们已有可查询的数据。

通过 API 创建 ES|QL 工具

我们将不使用 UI,而是通过 Agent Builder API 编程方式创建工具。这使版本控制和部署更方便。

首先,设置我们的 Kibana API 连接:

`

1.  import requests

3.  KIBANA_URL = os.environ["KIBANA_URL"]  # e.g., https://your-deployment.kb.us-central1.gcp.cloud.es.io
4.  KIBANA_API_KEY = os.environ["KIBANA_API_KEY"]

6.  headers = {
7.      "kbn-xsrf": "true",
8.      "Authorization": f"ApiKey {KIBANA_API_KEY}",
9.      "Content-Type": "application/json"
10.  }

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

工具 1:排行榜

该工具显示按击杀数排名的顶级玩家。我们使用 ES|QL 进行聚合:

`

1.  leaderboard_tool = {
2.      "id": "leaderboard",
3.      "type": "esql",
4.      "description": "Shows top players ranked by kills. Use when someone asks Who is the best? or Show me top players.",
5.      "configuration": {
6.          "query": """FROM player_stats
7.  | STATS total_kills = SUM(kills), total_wins = SUM(wins) BY username, hero, rank
8.  | SORT total_kills DESC
9.  | LIMIT 10""",
10.          "params": {}
11.      }
12.  }

14.  response = requests.post(
15.      f"{KIBANA_URL}/api/agent_builder/tools",
16.      headers=headers,
17.      json=leaderboard_tool
18.  )
19.  print(f"Leaderboard tool: {response.status_code}")

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

工具 2:英雄统计

英雄统计工具使用动态参数返回特定英雄的性能数据:

`

1.  hero_stats_tool = {
2.      "id": "hero_stats",
3.      "type": "esql",
4.      "description": "Gets win rate, pick rate, and tier for a specific hero. Use when someone asks How good is Mage? or What is the win rate for Warrior?",
5.      "configuration": {
6.          "query": """FROM hero_meta
7.  | WHERE hero_name == ?hero
8.  | KEEP hero_name, win_rate, pick_rate, tier, patch_version""",
9.          "params": {
10.              "hero": {
11.                  "type": "keyword",
12.                  "description": "The hero name to look up"
13.              }
14.          }
15.      }
16.  }

18.  response = requests.post(
19.      f"{KIBANA_URL}/api/agent_builder/tools",
20.      headers=headers,
21.      json=hero_stats_tool
22.  )
23.  print(f"Hero stats tool: {response.status_code}")

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

?hero 是一个动态参数。当 agent 调用该工具时,它会从用户的问题中填入英雄名称,将自然语言问题(如“Is Mage strong?”)转换为结构化查询,例如 | WHERE hero_name == “Mage”。

工具 3:Meta 报告

该工具显示当前版本中哪些英雄处于主导地位:

`

1.  meta_report_tool = {
2.      "id": "meta_report",
3.      "type": "esql",
4.      "description": "Shows all heroes sorted by tier and win rate. Use when someone asks What is the current meta? or Which heroes are S-tier?",
5.      "configuration": {
6.          "query": """FROM hero_meta
7.  | SORT tier ASC, win_rate DESC
8.  | KEEP hero_name, tier, win_rate, pick_rate""",
9.          "params": {}
10.      }
11.  }

13.  response = requests.post(
14.      f"{KIBANA_URL}/api/agent_builder/tools",
15.      headers=headers,
16.      json=meta_report_tool
17.  )
18.  print(f"Meta report tool: {response.status_code}")

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

添加索引搜索工具

对于非结构化内容,如常见问题和游戏机制,我们使用索引搜索工具。pattern 参数指定要搜索的 Elasticsearch 索引:

`

1.  game_knowledge_tool = {
2.      "id": "game_knowledge",
3.      "type": "index_search",
4.      "description": "Searches game guides, FAQs, and mechanics. Use when someone asks How do I...? or What is...? questions about game content.",
5.      "configuration": {
6.          "pattern": "game_knowledge"
7.      }
8.  }

10.  response = requests.post(
11.      f"{KIBANA_URL}/api/agent_builder/tools",
12.      headers=headers,
13.      json=game_knowledge_tool
14.  )
15.  print(f"Game knowledge tool: {response.status_code}")

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

请记住,ES|QL 工具用于结构化分析(“谁的击杀最多?”),而索引搜索工具用于非结构化知识(“如何解锁龙坐骑?”)。

创建 agent

现在我们来创建一个使用这些工具的 agent:

`

1.  agent = {
2.      "id": "gaming_support_bot",
3.      "name": "Gaming Support Bot",
4.      "description": "A gaming community support bot that answers player questions about stats, heroes, and game mechanics.",
5.      "configuration": {
6.          "tools": [{"tool_ids": ["leaderboard", "hero_stats", "meta_report", "game_knowledge"]}],
7.          "instructions": """You are a helpful gaming community bot. Answer player questions about:
8.  - Player stats and leaderboards (use leaderboard tool)
9.  - Hero performance and meta (use hero_stats and meta_report tools)
10.  - Game mechanics and guides (use game_knowledge tool)

12.  Be concise and friendly. Format leaderboards clearly with rankings."""
13.      }
14.  }

16.  response = requests.post(
17.      f"{KIBANA_URL}/api/agent_builder/agents",
18.      headers=headers,
19.      json=agent
20.  )
21.  print(f"Agent created: {response.status_code}")

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

该 agent 现在可以访问我们所有的工具,并根据用户的问题选择合适的工具。

你可以通过访问 Kibana 的 Agent Chat GUI 或发送 API 调用来测试连接性。默认情况下,Agent Builder 使用 Elastic 托管的 LLM,因此无需配置连接器:

`

1.  test_message = "Show me all heroes sorted by tier"

3.  response = requests.post(
4.      f"{KIBANA_URL}/api/agent_builder/converse",
5.      headers=headers,
6.      json={
7.          "agent_id": "gaming_support_bot",
8.          "input": test_message
9.      },
10.      timeout=60
11.  )

13.  print(f"Status: {response.status_code}")
14.  if response.status_code == 200:
15.      result = response.json()
16.      print(f"\nAgent used tools: {[step.get('tool_id') for step in result.get('steps', []) if step.get('type') == 'tool_call']}")
17.      print(f"\nResponse:\n{result.get('response', {}).get('message', 'No message')}")
18.  else:
19.      print(f"Error: {response.text}")

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

设置 Discord 机器人

如果你还没有 Discord 机器人,需要按照以下步骤创建:

  1. 访问 Discord 开发者门户(Discord Developer Portal)。

  2. 点击 “New Application”,并为应用命名。

  3. 进入 Bot 部分,点击 “Add Bot”。

  4. 复制 机器人的 token。(稍后会用到)

  5. Privileged Gateway Intents 下,启用 Message Content Intent

  6. 转到 OAuth2 > URL Generator,选择 botapplication.commands 范围,并勾选 Send MessagesRead Message History 权限。

  7. 使用生成的 URL 将 机器人 邀请到你的服务器。

安全注意事项

在 Discord 端,只请求最小权限:

  • Send Messages
  • Send Messages in Threads
  • Read Message History
  • Create Polls

这样即使 机器人 被入侵,其可执行的操作也有限。

Elastic 端,创建一个权限受限的 API key。对于这个 机器人,只需对游戏索引拥有读取权限:

`

1.  POST /_security/api_key
2.  {
3.    "name": "gaming-bot-key",
4.    "role_descriptors": {
5.      "gaming_bot_role": {
6.        "cluster": ["monitor"],
7.        "indices": [
8.          {
9.            "names": ["player_stats", "hero_meta", "game_knowledge"],
10.            "privileges": ["read"]
11.          }
12.        ]
13.      }
14.    }
15.  }

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

配置 Discord A2A 客户端

下一步是将 Discord 连接到 Agent Builder。我们将使用经过修改的 kagent-a2a-discord

克隆并进行设置:

`

1.  git clone https://github.com/llermaly/agentbuilder-a2a-discord
2.  cd agentbuilder-a2a-discord

`AI写代码

环境变量

在项目根目录下创建一个 .env 文件:

`

1.  DISCORD_BOT_TOKEN=your_bot_token
2.  AGENT_BUILDER_URL=https://<kibana_url>/api/agent_builder/a2a/gaming_support_bot
3.  A2A_API_KEY=your_api_key

`AI写代码

使用 Python 运行

`

1.  # Create virtual environment
2.  uv venv

4.  # Install dependencies
5.  uv sync

7.  # Run the bot 
8.  uv run main.py

`AI写代码

该机器人应该在几秒内上线你的 Discord 服务器。

测试机器人

我们来测试不同类型的查询。

分析查询(ES|QL)

用户:“谁是最强的 Mage 玩家?”

机器人:根据排行榜数据,ShadowMage 目前是游戏中最强的 Mage 玩家。他已达到大师段位,拥有 8,932 次击杀和 567 胜场……

用户:“当前的 meta 是什么?”

机器人:当前版本的 meta 由 Mage 主导,Mage 在 S 级独占,胜率达 54.7%,选择率为 22.8%,版本为 2.4.1。

语义查询(索引搜索)

用户:“如何解锁龙坐骑?”

机器人:要解锁龙坐骑,你需要:

  • 噩梦难度 完成龙之巢副本。
  • 保持所有队员在整个副本过程中存活。
  • 祝你好运——坐骑掉落率为 15%……

该 机器人 会根据问题自动选择正确的工具。

双向互动:赋予 agent 操作能力

除了回答问题外,我们还可以让 Agent Builder 触发 Discord 操作。通过对 Discord 客户端进行小幅修改,我们可以解析 agent 回复中的特殊标签,并执行相应的 Discord 命令。

例如,我们增加了对 标签的支持:

`<poll>Should Mage be nerfed?|Yes, too strong|No, it's balanced|Need more data</poll>`AI写代码

当 agent 在回复中包含该标签时, 机器人 会创建一个原生的 Discord 投票。agent 只需知道何时使用该标签即可。运行以下命令将其添加到指令中:

`

1.  agent = {
2.      "id": "gaming_support_bot",
3.      "name": "Gaming Support Bot",
4.      "description": "A gaming community support bot that answers player questions about stats, heroes, and game mechanics.",
5.      "configuration": {
6.          "tools": [{"tool_ids": ["leaderboard", "hero_stats", "meta_report", "game_knowledge"]}],
7.          "instructions": """You are a helpful gaming community bot. Answer player questions about:
8.  - Player stats and leaderboards (use leaderboard tool)
9.  - Hero performance and meta (use hero_stats and meta_report tools)
10.  - Game mechanics and guides (use game_knowledge tool)

12.  When discussing balance topics, create a poll for community input.
13.  Use: <poll>Question|Option1|Option2|Option3</poll>

15.  Be concise and friendly. Format leaderboards clearly with rankings."""
16.      }
17.  }

19.  response = requests.put(
20.      f"{KIBANA_URL}/api/agent_builder/agents",
21.      headers=headers,
22.      json=agent
23.  )
24.  print(f"Agent created: {response.status_code}")

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

现在,当用户询问 “Is Mage overpowered?” 时,agent 可以分享统计数据并创建投票,将数据洞察转化为社区互动。

同样的模式也适用于其他 Discord 功能,如表情反应、角色提及或定时公告。agent 的回复就成为了控制 Discord 操作的通道。

结论

在本文中,我们成功使用 Elastic Agent Builder 的 A2A 服务器构建了一个 Discord 机器人。过程包括通过 API 创建 ES|QL 工具用于分析(如排行榜、英雄统计和 meta 报告),以及开发索引搜索工具用于知识库的语义搜索。此外,我们演示了使用动态参数(如 ?hero 进行灵活查询)、建立 Discord 与 A2A 客户端的连接,并测试了分析查询和语义查询类型。

Agent Builder 提供了 A2A 服务器,因此你只需专注于创建工具和连接客户端,而 ES|QL 处理结构化分析,索引搜索处理非结构化知识。

资源

原文:www.elastic.co/search-labs…