Skip to main content
Glama

kaigo_mcp

将日本的介护供需数据,作为AI智能体可用的工具公开的MCP服务器。

从 Claude Code 或 Claude Desktop 连接后,就能基于公开数据回答“这个市的特别养护老人之家够不够”。

状态:服务器・智能体・eval 均已运行。 列A(本地7B)・列B(本地9B)・列D(Claude Haiku 4.5)已测量完毕。

这是做什么的

MCP(Model Context Protocol)是让LLM拥有工具的规范。本仓库构建的是 工具这一侧,不包含LLM本身。服务器单独运行时完全不调用API,也不会产生费用。

[考える側]                    [このリポジトリ]
Claude Code / 自作エージェント  ←stdio→  kaigo-gap サーバー
「どの道具を使うか」を判断              呼ばれたらデータを返す

Related MCP server: Japan Data MCP

工具

工具

用途

get_national_baseline

全国基准值。是评估个别数字的前提

lookup_insurer

按市区町村名・保险者名查询供需

rank_insurers

按特养不足程度(充足程度)排序

主要指标是每100名需要介护3级以上的认定者对应的特养定员数(全国24.6人・中位数26.7人)。 定义和出处位于 kaigo_gap_analysis 一侧。

汇总计算不在这里写

数据由案4(kaigo_gap_analysis)通过 export_web.py 导出, 与公开仪表盘 所分发的 相同的 insurers.json 复制后使用。这里不重新编写除法计算。

如果在两个地方持有指标定义,当智能体的回答与仪表盘的数字不一致时, 就无法判定哪一边是正确的。案4的 dataset.py 按照“定义只在一处”的方针 编写,因此跨项目遵守该方针。更新通过 python scripts/sync_data.py 执行。

工具设计上注意的事项

只返回数字无法做出判断。 即使把“12.4”交给LLM,它也不知道是高是低, 因此在全国排名和工具说明文中都包含了全国值。

不把同名自治体硬性固定为一件。 “府中市”在东京都和广岛县都有。 同时返回两个候选,筛选交由调用方决定。

不让特养定员0被解读为“全国最差”。 符合条件的保险者有90个,全部会并列第1名。 必须附带并列数量,并加上“在小规模自治体中并不罕见,居民多使用邻近自治体的设施”这一注释。 如果没有这个注释,就会从 rank_insurers 的结果得出“新乡村是全国最差的地区”这一错误结论。

如果不把领域知识嵌入工具的响应中,每次更换调用方的模型时, 同样的误读就会再次发生。之所以放在工具侧而不是提示词中,正是出于这个原因。

使用方法

pip install -r requirements.txt
python scripts/sync_data.py     # 案4からデータを取り込む
python scripts/smoke_test.py    # 通信の疎通確認

要从 Claude Code 使用,在此目录下启动即可读取 .mcp.json。 注册到其他客户端时的配置:

{
  "mcpServers": {
    "kaigo-gap": {
      "command": "python",
      "args": ["-m", "kaigo_mcp"],
      "cwd": "/path/to/kaigo_mcp"
    }
  }
}

写 Windows 路径时要用 \ 分隔("C:\projects\kaigo_mcp")。 在 JSON 中 \p 会成为非法转义,导致整个配置文件无法读取。 本仓库的 .mcp.json 实际上就因此坏过。

智能体

python -m kaigo_mcp.agent --list
python -m kaigo_mcp.agent "尼崎市は特養が足りてる?" --verbose

工具通过MCP服务器调用(不直接import函数)。如果用import解决, 就不算通过MCP,“创建了MCP服务器”这一主张就无法被验证。

用于比较的列

列

模型

位置

设置

每题成本

状态

A

qwen2.5:7b

主力机(CPU)

—

0日元

已测量

B

qwen3.5:9b

RTX 5050 8GB

num_ctx 4096(ollama默认)

0日元

已测量

B'

qwen3.5:9b

RTX 5050 8GB

num_ctx 8192

0日元

已测量

C

Qwen3.5-397B-A17B

DeepInfra

—

约0.8日元

保留(注册可能需要信用卡)

D

Claude Haiku 4.5

Anthropic

—

约1.4日元(实测)

已测量

各列之间的差异只对应一个因素。

比较

能了解到什么

A → B

硬件和模型世代的效果(已通过 probe 区分:质量取决于世代,速度取决于硬件)

B → B'

仅设置的效果。模型和权重完全相同

B' → C

仅模型规模的效果(同为 Qwen3.5 系列,已对齐)

C → D

模型系列的差异

同时保留 B 和 B',是因为只保留一边会丢失一些发现。 相同的模型・相同的权重,仅改变 num_ctx,正确率就分为 83% 和 100%。 只放 8k 会显得“本地9B很优秀”,只放 4k 会显得 “本地模型无法实用”。两者都不是真相。

列C处于保留状态。最初指的是 Qwen3-235B-A22B,但世代为3.0,与列B不一致, 而且提供也已终止。免费额度的目录会不断更换,因此使用前 要通过 /models 确认其确实存在。NVIDIA免费额度(C-alt-nvidia)也试过, 但有一次往返耗时168秒,无法用于 eval。

循环只写一条。 如果一边用SDK的工具运行器、另一边用手写, 列间的差异就无法区分是模型的差异还是循环实现的差异。 后端只负责历史记录的转换。

实测(列A・列B)

各6次・通过 python scripts/probe_tool_calling.py 测量。

列A→列B 中硬件和模型世代同时变化, 因此为了区分,将 7B 也在 GPU 机上运行(中间列)。

列A qwen2.5:7b (CPU)

qwen2.5:7b (GPU)

列B qwen3.5:9b (GPU)

工具调用成功率(默认温度)

4/6 = 67%

5/6 = 83%

6/6 = 100%

工具调用成功率(温度0)

6/6 = 100%

6/6 = 100%

6/6 = 100%

指令遵循(温度0)

0/6 = 0%

0/6 = 0%

6/6 = 100%

每次耗时(预热后)

7.6秒

0.4秒

3.2秒

首次(含模型加载)

102.6秒

36.1秒

19.2秒

需要温度0是 7B 特有的问题

qwen2.5:7b 在默认温度下 <tool_call> 的开始标签会损坏 (olith pering 等几个字符会出现在开头),ollama 的解析器无法识别, 调用会漏到正文文本中。只留下结束标签 </tool_call> 是标志。温度0可解决。

qwen3.5:9b 在默认温度下也是 6/6。 这种损坏方式不是本地运行的一般问题, 而是该模型的世代・规模所特有的。

即使把同一个 7B 放到 GPU 机上运行,默认温度下仍会漏(5/6), 连漏的方式都相同(brtc {"name": "lookup_insurer", ...} </tool_call> — 几个字符占据开始标签的位置,只留下结束标签)。更换硬件也无法修复。 这是模型侧的习惯。

更大的差异在于“是否遵循指令”

系统提示词指示“回答前要用 get_national_baseline 确认全国基准值”。 9B 有 6/6 遵循,7B 有 0/6 一次都没遵循。 而且 9B 会在一步中并行调用两个工具。

7B 能给出正确答案,是因为拾取了工具说明文中嵌入的全国值(24.6), 并没有按照指示的步骤执行。差异不在于稳定性,而在于指令遵循, 工具越多影响越大。

起作用的是模型世代,GPU只影响速度

即使把 7B 放到 GPU 上,质量指标都没有变化。 默认温度下的标签损坏仍然存在(4/6 → 5/6,n=6 所以在波动范围内), 温度0下的指令遵循仍是 0/6。GPU 机上的 7B 在默认温度下6次中只有2次 查询了基准值,温度0下一次都没查询。

变化的只有秒数,7.6秒 → 0.4秒(约19倍)。 也就是说 A→B 的差异中,成功率和指令遵循是模型世代的效果, 速度是硬件的效果。在同一张表中完成了区分。

顺便一提,9B 比 7B 慢8倍(3.2秒 vs 0.4秒)。 即使在同一 GPU 上,并行调用两个工具并遵循指令,输出也会更长。 速度与指令遵循在这里形成了正面的权衡。

对上次关于秒数修正的进一步验证

列B的“默认 18.9秒 → 温度0 4.4秒”不是温度的效果,而是首次加载。 用改进版脚本重新采集后,首次 19.2秒・之后 3.2秒(温度0下之后为 3.9秒), 预热后温度没有差异。与列A的首次 102.6秒・之后 7.6秒结构相同, 混入首次的平均值看起来既像温度差异又像硬件差异。

完整运行时,先掉的是上下文而不是智能

不是工具调用单独,而是智能体完整跑完一道题的结果。

“尼崎市的特养够吗?”

列A 7B (CPU)

列B 9B (4k)

列B' 9B (8k)

步骤

2

2

3

秒

72.31

75〜89

21.6〜37.2

输入 / 输出令牌

1,648 / 171

1,985 / 3,121

3,176 / 601

答案

正确

空白(0/4次)

正确(3/3次)

先触顶的不是 VRAM,而是 ollama 的默认 num_ctx = 4096。 原本预计 9B 占用 6.6GB 会导致 KV缓存紧张, 但在到达那一步之前就被默认的上下文长度截停了。

停止方式很难察觉。第2步以 stop_reason='length' 结束, 提示词约1,150 + 输出2,946 = 正好 4,096。输出有3,121个令牌但正文是空的。

触发原因是模型自身传了错误的参数。尼崎市明明在兵库县, 却传了 lookup_insurer({'name': '尼崎市', 'pref': '大阪'}),返回0条匹配, 然后思考了2,946个令牌,在开始写答案之前就用完了。 自我恢复所需的令牌吃光了上下文。

将 num_ctx 设为 8192 可以解决,但这次 ollama ps 显示 12%/88% CPU/GPU。 8GB 装不下。4k 下得不到答案,8k 下装不进 GPU, 这就是在 RTX 5050 8GB 上运行 9B 的实际情况。

难题(在青森县列举3个市町村并与全国比较・--max-steps 8) 在 4k 下也以3步・41.06秒完成。 决定上下文的不是问题的难度,而是是否需要恢复。

是测量本身的缺陷(已修复)

修复前的 loop.py 在无工具调用时将 stopped_by = "end"。 即使被 length 截断且正文为空也会记为 "end", 所以上面的4次在记录上以“2步完成”的形式,与列A的成功相同地留存。 如果空白答案与完成看起来一样,那么排列各列就没有意义。

现在如果 length 或正文为空,则标记 truncated。 汇总查看 RunRecord.answered。CLI 会输出原因并返回退出码1。

这类缺陷,不实际踩到失败的列就发现不了。 如果只跑列A,就不会注意到。

而且这个修复本身还残留了两个漏洞。两者都在跑列C・D的瞬间生效。

  • 表示截断的值各公司不同。 OpenAI兼容是 length,Anthropic 是 max_tokens。 由于只看了 length,在列D(Claude)中即使被截断也无法检测。 列B中把空白答案计为完成的那种损坏方式,会原样在列D中复发。

  • 没有考虑在工具调用中途被截断的情况。 因为判定 位于“无工具调用时”的条件内。如果参数JSON不完整就继续下一步, 只会留下工具失败的记录,原因看起来像是“模型不行”。

将 TRUNCATED_REASONS = {"length", "max_tokens"} 放在工具调用有无的判断之前。 将 max_tokens 设为 8(在工具之前截断)和 40(在工具调用中途截断)实际踩过, 同时确认了正常情况不会变成 truncated。

eval 的结果(列A・列B・列D)

6个案例・通过 python scripts/eval_agent.py 测量。判定全部使用 仅凭程序即可写出的条件(不使用 LLM-as-judge)。列B跑了3轮(18件)。

列A 7B (CPU)

列B 9B (4k)

列B' 9B (8k)

列D Haiku 4.5

正确率

5/6 = 83%

15/18 = 83%

18/18 = 100%

15/18 = 83%

完成率

6/6 = 100%

15/18 = 83%

18/18 = 100%

18/18 = 100%

指令遵循

2/6 = 33%

18/18 = 100%

18/18 = 100%

17/18 = 94%

每题秒数(中位数)

72〜85秒

16.9秒

24.0秒

6.5〜8.5秒

成本

0日元

0日元

0日元

25日元

num_ctx 不是速度设置,而是正确率设置

4k 的3次失分全部是 amagasaki-basic 的 truncated/length。 机制就是上一节(完整运行时,先掉的是上下文而不是智能) 所看到的原样出现在 eval 中,并不是新的损坏方式。

通过 eval 了解到的是,相同的模型・相同的权重, 仅凭设置就让正确率分为 83% 和 100%。 8k 只是给了恢复所需的余地,模型本身没有任何变化。

不能写“本地超越了 Haiku”

在这6个案例中列B'(8k)更优(正确率 18/18 对 15/18、 指令遵循 18/18 对 17/18)。但这样解读太勉强。

  • 只有6个案例×3轮。 样本量太小

  • Haiku 的失分并非性能所致。 如下面的“注释效果过强”所述, 包含读取了工具侧嵌入的注释后排除定员0的4件的轮次。 将源于我方设计的失分计为模型的弱点,是错误的

  • 秒数是3倍(24.0秒 对 6.5〜8.5秒)。不在同一舞台上

能说的是“在这6道题中,8k的本地9B够用了”为止。 考虑到0日元且不离开本地的条件,这本身就是足够的结果。

正确率相同但内容不同(列A)

列A・列B・列D 都是正确率 83%,但构成不同。 列A是拾取了工具说明文中嵌入的全国值而偶然答对, 并没有按照指示确认基准值(指令遵循 2/6)。 列B・列D每次都查询。工具越多,这个差异越明显。

列B 的失分是上述的 truncated,不是答不出来, 而是没能写完答案。同样是83%,但没答到的原因三者各不相同。 只排列正确率这一个数字,这个差异就会消失。

工具侧的注释有效,但效果过强

rank_insurers 的响应中嵌入了“定员0在小规模自治体中并不罕见,不要单独下结论”。 列D读取后,将定员0的4件从答案中排除了(3轮中1次)。 数字与真实数据一致,不是编造的。

误读是防住了,但取而代之的是信息被隐藏了。“不要下结论”被解读为“不要提及”。 将领域知识嵌入工具响应的判断是有效的,但有时会效果过强。

今后

  • 列B 决定同时保留 4k / 8k 两者(B 和 B')。因为仅凭设置就会产生差异,这正是主题

  • 列C 保留。DeepInfra 注册可能需要信用卡

  • NVIDIA免费额度无法用于 eval(速度没有保证。有往返168秒的情况)

相关

  • kaigo_matching — 介护记录的结构化提取(案1)

  • kaigo_gap_analysis — 按保险者的供需差距分析(案4・本数据的来源)

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    D
    maintenance
    Enables AI agents to query Japanese public data (laws, corporations, statistics) from official government APIs, returning normalized English metadata with source attribution.
    1
    MIT
  • A
    license
    Not graded
    quality
    B
    maintenance
    Enables AI assistants to search Japanese government subsidies and grants, including archived closed calls and recurring application periods based on historical data.
    10 npm
    1
    MIT