Elasticsearch构建实时语音助手

通过 Google ADK 和 MCP 协议,仅需 3 个组件即可将实时语音流连接到 Elasticsearch 数据,无需编写自定义集成代码。


1. 概述

大厨双手沾满油污,无法触碰屏幕,却急需快速查询菜品信息,比如"海鲜烩饭里有没有贝类?"或者"今晚最畅销的菜肴是什么?"------此时,一个能直接与 Elasticsearch 对话的语音助手就能派上大用场。

本文将手把手教你搭建这样一个实时语音助手,它基于 Google Agent Development Kit (ADK)Gemini LiveAPI ,通过 Agent Builder 内置的 MCP 服务器,将你的语音查询转化为对 Elasticsearch 数据的语义搜索,并将结果语音播报给你。

整个方案的核心优势在于:Agent Builder 原生提供了托管 MCP 服务器 ,因此任何兼容 MCP 协议的智能体(如 Google ADK、Claude Desktop、LangChain 等)都能直接查询 Elasticsearch,无需编写任何集成代码------Elasticsearch 相关的代码只有大约 30 行 Python

架构图

#mermaid-svg-fGRUQefmPRSTYAwm{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-fGRUQefmPRSTYAwm .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-fGRUQefmPRSTYAwm .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-fGRUQefmPRSTYAwm .error-icon{fill:#552222;}#mermaid-svg-fGRUQefmPRSTYAwm .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-fGRUQefmPRSTYAwm .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-fGRUQefmPRSTYAwm .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-fGRUQefmPRSTYAwm .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-fGRUQefmPRSTYAwm .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-fGRUQefmPRSTYAwm .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-fGRUQefmPRSTYAwm .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-fGRUQefmPRSTYAwm .marker{fill:#333333;stroke:#333333;}#mermaid-svg-fGRUQefmPRSTYAwm .marker.cross{stroke:#333333;}#mermaid-svg-fGRUQefmPRSTYAwm svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-fGRUQefmPRSTYAwm p{margin:0;}#mermaid-svg-fGRUQefmPRSTYAwm .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-fGRUQefmPRSTYAwm .cluster-label text{fill:#333;}#mermaid-svg-fGRUQefmPRSTYAwm .cluster-label span{color:#333;}#mermaid-svg-fGRUQefmPRSTYAwm .cluster-label span p{background-color:transparent;}#mermaid-svg-fGRUQefmPRSTYAwm .label text,#mermaid-svg-fGRUQefmPRSTYAwm span{fill:#333;color:#333;}#mermaid-svg-fGRUQefmPRSTYAwm .node rect,#mermaid-svg-fGRUQefmPRSTYAwm .node circle,#mermaid-svg-fGRUQefmPRSTYAwm .node ellipse,#mermaid-svg-fGRUQefmPRSTYAwm .node polygon,#mermaid-svg-fGRUQefmPRSTYAwm .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-fGRUQefmPRSTYAwm .rough-node .label text,#mermaid-svg-fGRUQefmPRSTYAwm .node .label text,#mermaid-svg-fGRUQefmPRSTYAwm .image-shape .label,#mermaid-svg-fGRUQefmPRSTYAwm .icon-shape .label{text-anchor:middle;}#mermaid-svg-fGRUQefmPRSTYAwm .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-fGRUQefmPRSTYAwm .rough-node .label,#mermaid-svg-fGRUQefmPRSTYAwm .node .label,#mermaid-svg-fGRUQefmPRSTYAwm .image-shape .label,#mermaid-svg-fGRUQefmPRSTYAwm .icon-shape .label{text-align:center;}#mermaid-svg-fGRUQefmPRSTYAwm .node.clickable{cursor:pointer;}#mermaid-svg-fGRUQefmPRSTYAwm .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-fGRUQefmPRSTYAwm .arrowheadPath{fill:#333333;}#mermaid-svg-fGRUQefmPRSTYAwm .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-fGRUQefmPRSTYAwm .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-fGRUQefmPRSTYAwm .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-fGRUQefmPRSTYAwm .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-fGRUQefmPRSTYAwm .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-fGRUQefmPRSTYAwm .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-fGRUQefmPRSTYAwm .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-fGRUQefmPRSTYAwm .cluster text{fill:#333;}#mermaid-svg-fGRUQefmPRSTYAwm .cluster span{color:#333;}#mermaid-svg-fGRUQefmPRSTYAwm div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-fGRUQefmPRSTYAwm .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-fGRUQefmPRSTYAwm rect.text{fill:none;stroke-width:0;}#mermaid-svg-fGRUQefmPRSTYAwm .icon-shape,#mermaid-svg-fGRUQefmPRSTYAwm .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-fGRUQefmPRSTYAwm .icon-shape p,#mermaid-svg-fGRUQefmPRSTYAwm .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-fGRUQefmPRSTYAwm .icon-shape .label rect,#mermaid-svg-fGRUQefmPRSTYAwm .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-fGRUQefmPRSTYAwm .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-fGRUQefmPRSTYAwm .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-fGRUQefmPRSTYAwm :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} MCP 协议
语义搜索
结果
MCP 响应
语音回答
👤 用户语音
🎤 语音智能体

Google ADK
🧩 Agent Builder

内置 MCP 服务器
📊 Elasticsearch

菜谱索引


2. 准备工作

在开始之前,请确保你具备以下条件:

  • Elasticsearch 9.0 或更高版本(推荐使用 Elastic Cloud Serverless)
  • Google AI API 密钥(可使用免费试用额度)
  • Python 3.10 或更高版本
  • Node.js (用于运行 mcp-remote 工具)
  • 已安装 google-adkgoogle-genaipython-dotenvpyaudio 等依赖库

3. 建立 Elasticsearch 语义搜索索引

我们的知识库是一个包含食谱数据的 Elasticsearch 索引,其中包括食材、过敏原、制作步骤等信息。这样就能支持自然语言查询,例如"不含乳制品的菜肴"或"如何制作招牌油醋汁?"

3.1 数据模型

我们使用一个索引,其文档结构如下(示例数据取自 knowledge.json):

json 复制代码
[
    {
      "name": "蘑菇烩饭",
      "ingredients": ["意大利米", "蘑菇", "帕玛森奶酪", "黄油", "白葡萄酒", "蔬菜高汤", "红葱头", "大蒜"],
      "allergens": ["乳制品"],
      "procedure": "黄油炒米,加入白葡萄酒收汁,分次加入热高汤搅拌,最后拌入炒好的蘑菇并撒上帕玛森奶酪。",
      "prep_time_minutes": 35,
      "category": "主菜",
      "dietary": ["素食"]
    },
    {
      "name": "海鲜烩饭",
      "ingredients": ["意大利米", "虾", "贻贝", "鱿鱼", "白葡萄酒", "鱼高汤", "大蒜", "欧芹", "黄油"],
      "allergens": ["贝类", "乳制品"],
      "procedure": "黄油大蒜炒米,加白葡萄酒收汁,分次加入热鱼高汤,最后几分钟加入海鲜,撒欧芹。",
      "prep_time_minutes": 40,
      "category": "主菜",
      "dietary": []
    },
    {
      "name": "招牌油醋汁",
      "ingredients": ["橄榄油", "红酒醋", "第戎芥末", "蜂蜜", "大蒜", "盐", "胡椒"],
      "allergens": [],
      "procedure": "将芥末、醋、蜂蜜和蒜末搅匀,缓慢倒入橄榄油并不断搅拌,加盐和胡椒调味。",
      "prep_time_minutes": 5,
      "category": "酱汁",
      "dietary": ["素食", "无麸质"]
    }
]

3.2 创建 API 密钥(Serverless 环境必需)

在 Elasticsearch Serverless 环境中,需要创建具有特定权限的 API 密钥:

python 复制代码
POST /_security/api_key
{
  "name": "google-adk-api-key",
  "expiration": "30d",
  "role_descriptors": {
    "mcp-access": {
      "cluster": ["all"],
      "indices": [
        {
          "names": ["*"],
          "privileges": ["all"],
          "allow_restricted_indices": false
        }
      ],
      "applications": [
        {
          "application": "kibana-.kibana",
          "privileges": [
            "feature_agentBuilder.all",
            "feature_actions.read",
            "feature_inference.all",
            "feature_advancedSettings.read"
          ],
          "resources": ["space:default"]
        }
      ]
    }
  }
}

3.3 创建推理端点(Inference Endpoint)

为了进行语义搜索,我们使用 jina-embeddings-v5-text-small 模型作为嵌入引擎:

python 复制代码
INFERENCE_ID = "jina-embeddings"

inference_config = {
    "service": "elastic",
    "service_settings": {"model_id": "jina-embeddings-v5-text-small"},
}

es_client.inference.put(
    task_type="text_embedding", 
    inference_id=INFERENCE_ID, 
    body=inference_config
)

3.4 创建索引映射

关键点在于 semantic_field,它使用了 semantic_text 类型,并关联了上面创建的推理端点。其他字段通过 copy_to 将内容复制到该字段,以便进行统一的语义搜索:

python 复制代码
knowledge_mapping = {
    "properties": {
        "name": {"type": "text", "copy_to": "semantic_field"},
        "ingredients": {"type": "text", "copy_to": "semantic_field"},
        "allergens": {"type": "keyword", "copy_to": "semantic_field"},
        "procedure": {"type": "text", "copy_to": "semantic_field"},
        "prep_time_minutes": {"type": "integer"},
        "category": {"type": "keyword", "copy_to": "semantic_field"},
        "dietary": {"type": "keyword", "copy_to": "semantic_field"},
        "semantic_field": {
            "type": "semantic_text",
            "inference_id": INFERENCE_ID,
        },
    }
}

3.5 批量导入数据

使用 Elasticsearch 的 Bulk API 将数据加载到索引中:

python 复制代码
from elasticsearch import helpers

def build_bulk_actions(documents, index_name):
    for doc in documents:
        yield {"_index": index_name, "_source": doc}

with open("dataset/knowledge.json", "r") as f:
    docs = json.load(f)

success, failed = helpers.bulk(
    es_client,
    build_bulk_actions(docs, "knowledge"),
    refresh=True,
)
print(f"{success} 个文档索引成功")

4. 使用 Agent Builder 创建搜索工具

我们将通过 Agent Builder 的 API 来编程式地创建一个语义搜索工具。这样做便于版本控制,且可重复使用。

4.1 启用 Agent Builder

  • 非 Serverless 集群:需按照官方文档启用 Agent Builder。
  • Serverless 环境:默认已启用。

注意:API 密钥必须包含 feature_agentBuilder.read 权限,否则无法访问 Agent Builder。

4.2 创建工具

以下代码使用 HTTP 请求创建名为 recipe_semantic_search 的工具:

python 复制代码
recipe_search_tool = {
    "id": "recipe_semantic_search",
    "type": "index_search",
    "description": "搜索厨房食谱,包括食材、过敏原、膳食限制、制作步骤和烹饪时间。使用语义搜索,即使没有精确关键词也能找到相关食谱。",
    "tags": ["semantic"],
    "configuration": {
        "pattern": "knowledge",   # 索引名称
    },
}

response = requests.post(
    f"{KIBANA_ENDPOINT}/api/agent_builder/tools",
    headers=KIBANA_HEADERS,
    json=recipe_search_tool,
)

关键点

  • type: "index_search" 表明这是一个索引搜索工具。
  • description 字段非常重要,它会引导智能体在合适的时候调用该工具。
  • tags 中的 semantic 表示启用语义搜索能力。

4.3 设置 Gemini 为默认模型

Agent Builder 默认使用 Anthropic Claude。由于我们使用 Gemini,需要在界面上手动调整:

  • 进入 Agent Builder 菜单 → GenAI Settings → 将 Default AI Connector 改为 Google Gemini 2.5 Flash
  • 在聊天界面中,确保模型下拉框也选择了相同的 Gemini 模型。

4.4 监控 Token 用量

Agent Builder 集成了 Kibana 仪表盘,可查看提示词 token、补全 token 和请求总数,按功能与模型细分,帮助你掌握用量。


5. 通过 MCP 连接 Google ADK 与 Elasticsearch

Google ADK 与 Agent Builder 之间通过 MCP(Model Context Protocol)连接,只需三个组件即可完成,总 Python 代码约 30 行

5.1 环境配置

确保已按照 Google ADK 的官方教程设置好语音/视频通信环境。然后,将 agent.py 文件内容替换为以下代码:

python 复制代码
import os
from dotenv import load_dotenv
from google.adk.agents import Agent
from google.adk.tools.mcp_tool import McpToolset
from google.adk.tools.mcp_tool.mcp_session_manager import StdioConnectionParams
from mcp.client.stdio import StdioServerParameters

load_dotenv()

KIBANA_ENDPOINT = os.getenv("KIBANA_ENDPOINT")
ELASTIC_API_KEY = os.getenv("ES_API_KEY")

AUTH_HEADER = f"ApiKey {ELASTIC_API_KEY}"

root_agent = Agent(
    model="gemini-2.5-flash-native-audio-latest",
    name="kitchen_assistant_agent",
    instruction="""你是一位厨房助手,在忙碌的晚餐时段帮助厨师。
    你可以回答关于食谱的问题。
    使用 Elasticsearch 工具搜索菜谱索引,快速提供答案,例如:
    - 某道菜是否含有特定过敏原(如贝类)?
    - 用给定食材可以准备哪些菜?
    - 我想做一道海鲜菜。
    - 按类别或膳食限制查找食谱。
    回答要简洁实用------厨师需要快速答案!""",
    tools=[
        McpToolset(
            connection_params=StdioConnectionParams(
                server_params=StdioServerParameters(
                    command="npx",
                    args=[
                        "-y",  # 自动确认安装
                        "mcp-remote",
                        f"{KIBANA_ENDPOINT}/api/agent_builder/mcp",
                        "--header",
                        f"Authorization:{AUTH_HEADER}",
                    ],
                ),
                timeout=30,
                session_read_timeout_seconds=120,
            ),
            tool_filter=["recipe_semantic_search"],  # 只使用这个工具
        )
    ],
)

5.2 三大组件解析

  1. Agent:定义语音助手的名称、模型、指令及可用工具。
  2. McpToolset:充当 MCP 客户端,让智能体可以连接任何 MCP 服务器并使用其暴露的工具。
  3. StdioConnectionParams :通过启动本地进程(mcp-remote)建立与 Agent Builder MCP 端点的连接,负责通信桥接。

关于模型选择:我们使用 gemini-2.5-flash-native-audio-latest,这是专为 Live API 优化的 Gemini 模型,支持原生音频输入输出,无需中间文本转换,实现低延迟实时对话。


6. 运行与测试

6.1 启动应用

确保 .env 文件已正确设置以下变量:

复制代码
KIBANA_ENDPOINT=https://your-elastic-cloud-instance
ES_API_KEY=your_api_key

安装依赖:

bash 复制代码
pip install google-adk google-genai python-dotenv pyaudio

启动 Web 界面:

bash 复制代码
adk web --port 8000

提示:Live API 首次响应可能需要约 30 秒,ADK Web 界面在后台处理时不会显示进度指示。

6.2 在 Web 界面选择智能体

打开浏览器访问 http://localhost:8000,在左侧下拉菜单中找到 kitchen_assistant_agent 并选中,即可开始语音或文本交互。

6.3 提问示例

以下是一些典型问题及预期回答:

你说 回答
"海鲜烩饭里有贝类吗?" "海鲜烩饭包含虾和贻贝,属于贝类。"
"招牌油醋汁怎么做?" "将第戎芥末、红酒醋、蜂蜜和蒜末搅匀,缓慢倒入橄榄油并不断搅拌,最后加盐和胡椒调味。"
"有什么素食菜品推荐?" "素食佛陀碗、水果雪芭都是素食,招牌油醋汁也是纯素。"
"有没有无坚果的甜点?" "熔岩巧克力蛋糕、水果雪芭、意式奶冻都是无坚果的。"

6.4 调试与事件查看

ADK Web 界面左侧的 Events 视图可以查看智能体的每一次函数调用与响应。例如,当询问"素食食谱"时,你会看到:

  • Function Callrecipe_semantic_searchnlQuery 参数为 "vegan recipes"
  • Function Response:返回语义搜索命中的菜谱列表,如"素食佛陀碗""招牌油醋汁""水果雪芭"等。

6.5 进阶:使用摄像头进行多模态查询

如果你启用摄像头,可以像演示中那样,将手写订单拍照,智能体会逐项检查每道菜是否含某种过敏原。ADK 会为每道菜分别发起语义搜索请求,并按订单顺序依次回答------这展示了多模态输入与语义搜索结合的巨大潜力。


7. 总结

利用三项技术协作,成功构建了一个能与 Elasticsearch 对话的实时语音助手:

组件 作用
Elastic Agent Builder 提供语义搜索层,并内置托管 MCP 服务器
Google ADK + LiveAPI 实现双向实时语音流传输
MCP 协议 作为标准协议,连接 ADK 与 Agent Builder

关键洞察:Agent Builder 开箱即用就包含了 MCP 服务器,因此任何兼容 MCP 的智能体(Google ADK、Claude Desktop、LangChain 等)都能直接与 Elasticsearch 数据交互,无需编写自定义集成代码。这使得构建 AI 驱动的数据助手变得前所未有的简单。


参考资料