1. 为什么要在 Serverless 上跑向量检索 MCP Server
如果你正在给 AI Agent 接一套语义检索能力,大概率会遇到三个绕不开的问题:向量库要常驻、MCP Server 要鉴权、模型调用要单独配 Key。传统做法是买一台常开的机器,把 OpenSearch 客户端、嵌入模型调用、MCP 协议处理全塞进去,流量低谷时资源空转,流量高峰时又扛不住。Serverless 架构正好解决这个矛盾——请求来了才计费,没人用就缩到零。
MCP Server 是什么?简单说,它是 Model Context Protocol 的服务端实现,把「工具」以标准协议暴露给 Claude、Cline、Codex 这类客户端。Agent 不需要知道你的向量库在哪、用什么嵌入模型,只要按 MCP 协议发 JSON-RPC 请求,就能拿到相似度检索结果。适合谁?适合正在做 RAG、知识库问答、智能推荐的开发者,尤其是希望零运维、按量付费的团队。
向量检索这块,OpenSearch 的 k-NN 插件支持 FAISS、NMSLIB、Lucene 三种引擎,HNSW 和 IVF 算法都有,knn_vector字段配上余弦、内积、欧氏距离,几十亿向量也能做到毫秒级响应。把它放在 Lambda + API Gateway 后面,再挂一个 DynamoDB 管会话状态,整套链路就是纯 Serverless 的。
但这里有个容易被忽略的环节:鉴权与模型通道。MCP Server 要调用嵌入模型把文本转成向量,还要校验客户端传来的 token。如果每个环节都单独申请 Key、单独配环境变量,联调成本会很高。我这次的做法是用 TaoToken 统一 Key 接入,把模型调用和 MCP 鉴权收敛到一条 API 通道上,本地联调和线上部署用同一套凭证,省掉大量切换成本。下面从架构到可复制配置一步步拆。
2. TaoToken 前置准备:统一 Key 与 MCP 鉴权通道
在动手写 Lambda 之前,先把凭证体系理清楚。MCP Server 的鉴权分两层:一层是客户端到 MCP Server 的 token 校验(API Gateway 自定义授权器负责),另一层是 MCP Server 内部调用嵌入模型时的 API Key。传统做法是这两层各管各的,环境变量一堆,本地调试还要单独 mock。用 TaoToken 的好处是模型调用走统一通道,Key 只需要维护一份。
TaoToken 是什么?它是一个统一的大模型 API 接入通道,兼容 OpenAI 风格的接口格式,嵌入模型、对话模型都能通过同一个 Base URL 和 Key 调用。对 MCP Server 来说,这意味着generate_embedding函数里的EMBEDDING_API_URL和Authorization头可以固定下来,不用为每个模型单独适配。适合谁?适合需要在一个项目里调用多种模型、又不想管理多套凭证的开发者。
具体操作上,你需要先拿到一个 API Key。访问控制台创建:
https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite创建完 Key 后,在 API Keys 页面可以查看和管理:
https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite这里有个细节要注意:MCP Server 的鉴权 token 和 TaoToken 的 API Key 是两个不同的东西。前者是你自己定义的、用于 API Gateway 授权器校验的字符串(比如MCP_AUTH_TOKEN),后者是调用嵌入模型时用的。不要混用,也不要把 TaoToken 的 Key 直接当成 MCP 客户端的 auth token,否则一旦客户端泄露,模型额度也会被滥用。
接入文档在这里,建议先扫一遍接口格式:
https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite如果你打算长期跑编码类 Agent,或者需要更稳定的调用配额,可以了解下 Coding Plan:
https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite实测下来,把嵌入模型的调用统一到 TaoToken 后,Lambda 环境变量从原来的五六个缩减到三个:MCP_AUTH_TOKEN、TAOTOKEN_API_KEY、OPENSEARCH_ENDPOINT。本地联调时只需要在.env里填这三个,部署时通过 SAM 参数注入,不用改代码。
还有一点:MCP 协议本身不规定鉴权方式,API Gateway 的自定义授权器是最常见的做法。授权器 Lambda 收到请求后,从authorizationToken里取出客户端传来的 token,和MCP_AUTH_TOKEN比对,匹配就返回 Allow policy,否则 Deny。这个逻辑很简单,但要注意methodArn的构造,写错了会导致所有请求都被拒。
3. 可复制配置:Serverless 函数、MCP 工具定义与 OpenSearch 索引映射
这一节是核心,直接给可复制的配置片段。先看 OpenSearch 索引映射,这是向量检索的地基。knn_vector字段的dimension必须和嵌入模型输出维度一致,BGE-M3 是 1024 维,写错了索引创建会失败。
{ "settings": { "index": { "knn": true, "knn.algo_param.ef_search": 100 } }, "mappings": { "properties": { "document_id": { "type": "keyword" }, "text": { "type": "text" }, "metadata": { "type": "object", "enabled": false }, "embedding": { "type": "knn_vector", "dimension": 1024, "method": { "name": "hnsw", "space_type": "cosinesimil", "engine": "faiss", "parameters": { "ef_construction": 128, "m": 16 } } } } } }用 curl 创建索引:
curl -X PUT "https://your-opensearch-endpoint/vector-index" \ -H "Content-Type: application/json" \ -u "username:password" \ -d @index-mapping.json接下来是 Lambda 的 MCP 工具定义。基于LambdaMCPServer类,用@tool()装饰器注册两个工具:一个负责索引文本,一个负责相似度检索。注意类型提示要写全,MCP 客户端靠它生成输入 schema。
from typing import Dict from lambda_mcp_server import LambdaMCPServer mcp_server = LambdaMCPServer() @mcp_server.tool() def index_text_with_embedding( text: str, document_id: str = None, metadata: str = "{}" ) -> Dict: """将文本转换为向量并索引到知识库中""" embedding_result = generate_embedding(text) if embedding_result["status"] != "success": return {"status": "error", "message": embedding_result["message"]} doc = { "document_id": document_id or str(uuid.uuid4()), "text": text, "metadata": json.loads(metadata), "embedding": embedding_result["embedding"] } return opensearch_client.write_document(doc["document_id"], doc) @mcp_server.tool() def text_similarity_search( text: str, k: int = 10, score: float = 0.0 ) -> Dict: """通过向量相似度搜索相关文档""" embedding_result = generate_embedding(text) if embedding_result["status"] != "success": return {"status": "error", "message": embedding_result["message"]} return opensearch_client.search_documents( embedding_result["embedding"], k, score )嵌入函数走 TaoToken 统一通道,Base URL 和 Key 从环境变量读:
import os import requests TAOTOKEN_API_KEY = os.environ.get("TAOTOKEN_API_KEY") EMBEDDING_API_URL = "https://taotoken.net/api/v1/embeddings" DEFAULT_MODEL = "bge-m3" def generate_embedding(text: str, model: str = None) -> Dict: model_name = model or DEFAULT_MODEL payload = { "model": model_name, "input": text, "encoding_format": "float" } headers = { "Authorization": f"Bearer {TAOTOKEN_API_KEY}", "Content-Type": "application/json" } try: response = requests.post( EMBEDDING_API_URL, json=payload, headers=headers, timeout=30 ) response.raise_for_status() result = response.json() return { "status": "success", "embedding": result["data"][0]["embedding"], "model": model_name } except Exception as e: return {"status": "error", "message": f"API请求失败: {str(e)}"}API Gateway 自定义授权器 Lambda:
import os def lambda_handler(event, context): token = event.get("authorizationToken") expected_token = os.environ.get("MCP_AUTH_TOKEN") if token == expected_token: return generate_policy("Allow", event["methodArn"]) return generate_policy("Deny", event["methodArn"]) def generate_policy(effect, resource): return { "principalId": "mcp-client", "policyDocument": { "Version": "2012-10-17", "Statement": [{ "Action": "execute-api:Invoke", "Effect": effect, "Resource": resource }] } }SAM 模板里把参数串起来,部署时一次性注入:
Parameters: McpAuthToken: Type: String NoEcho: true TaoTokenApiKey: Type: String NoEcho: true OpenSearchEndpoint: Type: String Resources: McpFunction: Type: AWS::Serverless::Function Properties: Handler: app.lambda_handler Runtime: python3.11 Environment: Variables: MCP_AUTH_TOKEN: !Ref McpAuthToken TAOTOKEN_API_KEY: !Ref TaoTokenApiKey OPENSEARCH_ENDPOINT: !Ref OpenSearchEndpoint部署命令:
sam build sam deploy --guided \ --parameter-overrides \ "McpAuthToken=your-mcp-token" \ "TaoTokenApiKey=your-taotoken-key" \ "OpenSearchEndpoint=your-opensearch-endpoint"这里有个坑:NoEcho: true会让参数在 CloudFormation 控制台不显示明文,但部署日志里仍可能打印,建议用--parameter-overrides从环境变量读取,不要硬编码在脚本里。
4. 验证请求:curl 跑通检索链路与成功结果
部署完成后,先别急着接 MCP 客户端,用 curl 直接打 API Gateway 的 endpoint,确认鉴权和检索链路都通。MCP 协议走的是 JSON-RPC 2.0,请求体里method是tools/call,params里带工具名和参数。
先测tools/list,确认工具注册成功:
curl -X POST "https://your-api-id.execute-api.region.amazonaws.com/prod/mcp" \ -H "Content-Type: application/json" \ -H "Authorization: your-mcp-token" \ -d '{ "jsonrpc": "2.0", "id": 1, "method": "tools/list", "params": {} }'正常返回应该包含两个工具的定义:
{ "jsonrpc": "2.0", "id": 1, "result": { "tools": [ { "name": "index_text_with_embedding", "description": "将文本转换为向量并索引到知识库中", "inputSchema": { "type": "object", "properties": { "text": {"type": "string"}, "document_id": {"type": "string"}, "metadata": {"type": "string"} }, "required": ["text"] } }, { "name": "text_similarity_search", "description": "通过向量相似度搜索相关文档", "inputSchema": { "type": "object", "properties": { "text": {"type": "string"}, "k": {"type": "integer"}, "score": {"type": "number"} }, "required": ["text"] } } ] } }接着索引一条文档:
curl -X POST "https://your-api-id.execute-api.region.amazonaws.com/prod/mcp" \ -H "Content-Type: application/json" \ -H "Authorization: your-mcp-token" \ -d '{ "jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": { "name": "index_text_with_embedding", "arguments": { "text": "厄尔尼诺监测系统用于追踪太平洋海温异常", "document_id": "doc-001", "metadata": "{\"source\":\"test\"}" } } }'返回result.content[0].text里应该有status: success和写入的 document_id。然后做相似度检索:
curl -X POST "https://your-api-id.execute-api.region.amazonaws.com/prod/mcp" \ -H "Content-Type: application/json" \ -H "Authorization: your-mcp-token" \ -d '{ "jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": { "name": "text_similarity_search", "arguments": { "text": "太平洋海温异常监测", "k": 5, "score": 0.5 } } }'成功的话会返回匹配的文档列表,包含document_id、text和score。如果score阈值设太高,可能返回空数组,这时候把score降到 0.3 再试。实测下来,BGE-M3 对中文语义的区分度不错,同义改写能拿到 0.7 以上的相似度。
验证嵌入模型通道是否走通,可以单独打一次 TaoToken 的接口:
curl -X POST "https://taotoken.net/api/v1/embeddings" \ -H "Authorization: Bearer your-taotoken-key" \ -H "Content-Type: application/json" \ -d '{ "model": "bge-m3", "input": "测试文本", "encoding_format": "float" }'返回的data[0].embedding应该是 1024 维的浮点数组。如果这里报 401,说明 TaoToken 的 Key 有问题;如果 MCP 那边报 401,说明MCP_AUTH_TOKEN不匹配。两者要分开排查。
5. 本篇常见错排查:401、local proxy failed 与 reading choices
联调阶段最容易卡在几个报错上,逐个说清楚。
401 Unauthorized(MCP 层):curl 返回{"message":"Unauthorized"},说明 API Gateway 授权器拒绝了请求。先确认Authorization头有没有带,值是不是和MCP_AUTH_TOKEN完全一致。注意授权器 Lambda 里比对的是event.get("authorizationToken"),API Gateway 会把Authorization头的值原样传进来,不会自动去掉Bearer前缀。如果你在客户端加了Bearer,授权器里也要相应处理,否则永远不匹配。
401 Unauthorized(TaoToken 层):Lambda 日志里出现API请求失败: 401 Client Error,说明TAOTOKEN_API_KEY无效或过期。去控制台重新生成一个,更新 SAM 参数后重新部署。注意环境变量更新后 Lambda 需要重新部署才生效,改控制台环境变量也可以,但容易和 SAM 模板不一致,建议统一走部署流程。
local proxy failed:这个报错通常出现在本地用 MCP 客户端连远程 endpoint 时。原因是客户端配置了本地代理,但代理没启动或端口不对。检查客户端的 proxy 设置,把http_proxy、https_proxy环境变量清掉,或者确认代理服务在运行。MCP 客户端连 API Gateway 走的是标准 HTTPS,不需要额外代理。
reading choices 报错:这个一般出现在解析嵌入模型返回时。如果 TaoToken 返回的data数组为空,result["data"][0]会抛 IndexError,日志里可能显示成reading 'choices'之类的变体。根因是请求体格式不对,比如input传了空字符串,或者model名字写错。加一层防御:
data = result.get("data", []) if not data: return {"status": "error", "message": "嵌入返回为空,检查 input 和 model"}OAuth 相关报错:如果你用的 MCP 客户端要求 OAuth 流程,而你的 Serverless MCP Server 只做了 token 鉴权,会报OAuth not supported或类似错误。解决办法是在客户端配置里选择 token 鉴权模式,或者把 API Gateway 授权器改成支持 OAuth 的 Cognito 授权器。对于内部工具场景,token 鉴权足够,不用上 OAuth。
CC Switch / Cline MCP / Codex auth.json 配置:如果你用 Cline 或 Claude Code 这类客户端接 MCP Server,配置里要写全三件套——Base URL、Key、Model ID。以 Cline 的 MCP 配置为例:
{ "mcpServers": { "vector-search": { "url": "https://your-api-id.execute-api.region.amazonaws.com/prod/mcp", "headers": { "Authorization": "your-mcp-token" } } } }Codex 的auth.json里则是:
{ "mcp": { "vector-search": { "baseUrl": "https://your-api-id.execute-api.region.amazonaws.com/prod/mcp", "apiKey": "your-mcp-token", "model": "bge-m3" } } }注意这里的model是嵌入模型 ID,不是对话模型。写错了会导致检索时嵌入维度不匹配,OpenSearch 直接报dimension mismatch。
OpenSearch 连接超时:Lambda 默认超时 3 秒,OpenSearch 冷启动或网络抖动时容易超时。把 Lambda 超时调到 30 秒,VPC 配置确认能访问 OpenSearch 终端节点。如果 OpenSearch 开了细粒度访问控制,username和password要写对,否则报 403。
6. 语义一致 CTA:把统一 Key 接入落到你的项目里
整套链路跑通后,你会发现最省心的部分其实是凭证收敛。MCP Server 的鉴权 token 自己定义,嵌入模型的调用走 TaoToken 统一通道,本地联调和线上部署用同一套环境变量,不用在多个平台之间来回切换。对于需要频繁调试嵌入模型、又不想每次改代码的团队,这个模式能省下不少时间。
如果你还没拿到 Key,从控制台开始:
https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite接口格式和参数说明在文档里,嵌入模型的input、model、encoding_format都有示例:
https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite想先在网页上试一下模型对话效果,可以直接开对话页:
https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_content=model-chat&utm_campaign=rewrite长期跑编码类 Agent 或需要稳定配额的话,Coding Plan 更合适:
https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite最后留一个实用技巧:Lambda 里调用嵌入模型时,加一层本地缓存。同样的文本重复索引时,直接读缓存,省掉一次 API 调用。用functools.lru_cache或者 DynamoDB 做持久化缓存都行,实测能减少三成左右的嵌入调用量。OpenSearch 的knn_vector字段一旦创建就不能改维度,建索引前务必确认嵌入模型的输出维度,BGE-M3 是 1024,别的模型可能是 768 或 1536,写错了只能删索引重建。