两天开发六个 MCP 组合工具,我踩了六个坑
一句话摘要:在 stock-mcp-gateway 上两天内开发了持仓风险诊断、相关性矩阵、组合全景报告、调仓建议、个股评分、市场总览六个组合工具。本文拆解开发中遭遇的六个真实坑——从并行缓存竞态到跨市场日期对齐——以及每个坑的修复方案。
1. 缘起 — 工具是做不完的
我的 stock-mcp-gateway 是一个 Cloudflare Workers 上的 MCP(Model Context Protocol)网关,提供股票行情、技术分析、资金流向等基础数据工具集。每个工具职责单一、数据源明确——get_realtime_quote 负责行情,get_technical_analysis 负责技术指标,get_kline 负责 K 线数据。
但用户的需求从来不是”给我茅台的历史 K 线”,而是”我的持仓怎么样?该不该调仓?”。
这就意味着我需要组合工具(Composite Tools)——它们不直接从 API 抓原始数据,而是调多个已有工具/API,将数据聚合、计算、分析后返回结构化报告。
2026 年 7 月 27 日到 28 日,我集中开发了 6 个组合工具:
| 工具 | 功能 | 数据源数量 |
|---|---|---|
portfolio_risk_diagnosis |
持仓风险诊断 | 3(行情+名称+板块) |
portfolio_correlation |
持仓相关性矩阵 | 2(K线+名称) |
portfolio_full_report |
组合全景报告 | 4(以上全部+成本数据) |
portfolio_rebalance |
组合调仓建议 | 调用前两个工具 |
stock_score |
个股综合评分 | 3(行情+技术+资金) |
market_overview |
全市场总览 | 3(指数+北向+行业) |
听起来就是 Promise.all 一把梭的活儿?实际开发中的坑一个接一个。下面是我遭遇的六个真实问题。
2. 坑一:并行缓存竞态 — 价格全变成了 0
问题现象:portfolio_risk_diagnosis 工具返回的持仓中,部分股票当前价格为 0,导致盈亏计算全部错误。
排查过程:这些股票明明有实时行情数据。单独调用 get_realtime_quote("600519") 返回正常。但组合工具里读出来就是 0。
根因:我在 fetchPositionQuote() 函数里加了一个工具层缓存:
1 | // ❌ 错误写法 — 工具层缓存导致竞态 |
当 Promise.all 同时发起 N 个 fetchPositionQuote 调用时,如果前一个 batch 的 resolveName() 内部也调用了 tencent.getRealtimeQuote(通过 getStockInfo),它写入缓存时的 key 格式不同。于是当前 batch 的 fetchPositionQuote 读到了由另一个函数写入的、不同 key 格式的缓存条目,解析失败 → 返回 0。
修复方案:去掉工具层缓存。tencent.getRealtimeQuote() 自身已经有内部缓存(15 秒 TTL,由 src/cache.ts 管理),工具层再加一层不仅是重复,还引入了竞态风险。
1 | // ✅ 正确写法 — 依赖源模块的内置缓存 |
教训:缓存不是越多越好。每层缓存都有自己的 key 空间和生命周期,叠起来就是竞态温床。让数据源模块管理自己的缓存,组合工具只做编排不自己做缓存。
3. 坑二:Cloudflare Workers 累积超时
问题现象:当持仓数量 >= 8 只时,portfolio_correlation 工具在 Cloudflare Workers 上超时(30 秒 CPU 限制)。
排查过程:portfolio_correlation 需要拉取每只持仓的 K 线数据 + 每只持仓的名称。最开始的实现是:
1 | // ❌ 错误写法 — 串行叠加 |
看起来还好?不对——fetchKline 冷启动可能 2 秒(60 天 K 线首次拉取),8 只 × 2.2s + 8 × 100ms ≈ 18.4 秒,加上其他数据处理,轻松超过 25 秒 MCP 超时。
修复方案:采用混合并发模式——把轻量级请求(名称查询)提前发起,让它们在后台跑,同时串行执行重量级请求(K 线):
1 | // ✅ 正确写法 — 轻量并发 + 重量串行 |
为什么这有效:JavaScript 的事件循环在 await 阻塞时会继续执行已开始的 Promise。当第一个 fetchKline 在等待网络响应时,resolveName 的 Promise 已经在事件循环中推进了。到串行循环结束时,name 查询往往已经完成。总时间 ≈ N_serial × per_call + max(parallel_time, 0),而不是 N_serial × per_call + parallel_time。
教训:不要把所有 await 写在一起。能并行的先发起(不 await),必须串行的后执行(await),最后收集并行结果——这个顺序对总延迟有质的影响。
4. 坑三:跨市场日期对齐
问题现象:portfolio_correlation 计算出的相关性矩阵中,A 股和港美股的相关性经常为 NaN。
排查过程:持仓包含 A 股(159949)、港股(00700)、美股(AAPL)。三个市场的交易历完全不同:
- A 股:春节、国庆休市
- 港股:春节 + 复活节 + 佛诞日
- 美股:感恩节、圣诞
我最初的做法是把所有交易日的收盘价拼在一起,但某只股票某天没交易就填 null → 后续 Pearson 相关系数计算时 null 传播 → 全部 NaN。
修复方案:按共同交易日对齐——只保留所有持仓都有交易数据的日期:
1 | // 从各持仓的 K 线构建日期→价格映射 |
一个验证数据:用对齐后的数据计算,茅台和五粮液的 Pearson 相关系数 r = +0.81(同行业白酒,高度相关),而茅台和宁德时代 r = +0.23(不同行业,弱相关)。
教训:跨市场分析中,”数据对齐”不是技术细节,而是正确性的前提。不对齐就计算,结果全是噪声。
5. 坑四:单只股票失败拖垮整个组合
问题现象:当某只股票临时停牌或数据源返回异常时,整个 portfolio_risk_diagnosis 工具报错。
排查过程:Promise.all 的默认行为是所有或全无(fail-fast)——任何一个 promise reject,整个 Promise.all 就 reject。
1 | // ❌ 错误写法 — 一个失败全部崩 |
修复方案:每只股票的抓取链各自 try/catch,单只失败时返回含默认值的占位对象:
1 | // ✅ 正确写法 — 逐只隔离 |
教训:组合工具应遵守局部失败不全局崩溃原则。用 try/catch 包裹每个子任务,让调用方看到”数据缺失”而不是”500 错误”。
6. 坑五:source=bridge 的死胡同
问题现象:portfolio_full_report 工具需要从 Bridge(本地服务)获取持仓的成本数据,但在 Cloudflare Workers 上无法访问 localhost。
排查过程:fetchHoldingsFromBridge() 在网关代码中试图请求 http://localhost:9877/config/holdings。在本地开发环境没问题,部署到 Cloudflare Workers 后——localhost 指向 Workers 运行环境的本地,不是我的服务器。连接被拒绝。
修复方案:采用两步本地流程(two-step local flow):
- 本地脚本(
~/.hermes/scripts/portfolio-daily-briefing.py)在本地服务器运行,调用 Bridge 获取成本数据 - 脚本将数据传给 Gateway,再调用组合工具生成报告
- 整个流程通过 Hermes cron 在本地执行,
no_agent=true
这个模式是:Worker 只读取公网可访问的数据,本地私有数据在本地先准备好再传给 Worker。
教训:Cloudflare Workers 是边缘环境,没有”本地网络”。任何需要访问内网服务的数据流都要提前设计——要么走 Webhook 反向推送,要么走本地脚本编排。
7. 坑六:工具级超时保护
问题现象:portfolio_correlation 在处理 15+ 只持仓、60 天 K 线时,偶尔超过 Cloudflare Workers 的 30 秒 CPU 时间限制,返回 524 错误。
排查过程:MCP 协议本身没有工具级超时——一次 tools/call 可以永远等下去。但 Workers 平台有硬限制(免费计划 30 秒 CPU,付费计划 60 秒)。超过限制不会返回 MCP 错误码,而是 HTTP 524(超时)。
修复方案:用 Promise.race 实现工具级超时保护:
1 | const CORRELATION_TIMEOUT = 50000; // 50 秒 |
关键设计决策:
- 核心逻辑提取到
doCalcCorrelation(),让超时包装干净分离 portfolio_full_report中,相关性计算失败时不阻塞主报告——只把相关性部分设默认值
1 | // 在 full report 中:相关性失败不阻塞主报告 |
教训:在受限环境(Workers、Lambda、Deno Deploy)中,每个组合工具都应该有自己的超时保护。而且超时要优雅降级——宁可返回”相关性数据暂缺”,也不要抛 500 让用户什么都看不到。
8. 总结 — 组合工具的本质是编排
两天开发六个组合工具,真正的代码逻辑其实很简单——查数据、算指标、组合结果。难的不是写代码,是让代码在边缘环境中稳定运行。
几个核心 takeaway:
- 不要做自己的缓存 — 依赖数据源模块的缓存,工具层只做编排。层叠缓存是竞态的温床。
- 轻量先并行,重量后串行 — 利用 JS 事件循环的特性,在 await 重量操作时让轻量操作在后台推进。总时间降到接近最慢的串行操作。
- 局部失败不全局崩溃 — 每条数据链单独 try/catch,用占位对象替代抛异常。
- 跨市场先对齐日期 — 不对齐的计算结果全是噪声。
- 边缘环境没有”本地” — 内网数据走本地脚本编排,Worker 只访问公网。
- 每个工具都要有超时保护 — 优雅降级优于 HTTP 524。
这些工具上线后,每天收盘后自动跑一次组合诊断,生成持仓风险报告。整个过程约 6-8 秒(含缓存预热),用户可以收到带结构化风险标记的飞书卡片。
相关链接
- MCP Specification — MCP 协议规范
- Cloudflare Workers Limits — Workers 平台限制说明
- MCP Python SDK — MCP Python SDK
- 之前关于 DAG 智能路由的文章 — 如何在 MCP 网关中做模型路由
- 之前关于对抗式辩论的文章 — 多空辩论如何做持仓诊断






