通过 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-adk、google-genai、python-dotenv、pyaudio等依赖库
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 三大组件解析
- Agent:定义语音助手的名称、模型、指令及可用工具。
- McpToolset:充当 MCP 客户端,让智能体可以连接任何 MCP 服务器并使用其暴露的工具。
- 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 Call :
recipe_semantic_search的nlQuery参数为"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 驱动的数据助手变得前所未有的简单。