laya-mcp
by WayneCommand
README.md
<div align="center">
# laya-mcp
**把 Laya System-1 决策模型封装成一个工程化的 MCP Server(FastAPI 宿主)**
一次前向传播回答一组「带类型的问题」,返回**校准概率** + **可直接分支执行的决策契约**,
让任意支持 MCP 的 Agent(Claude Code / Codex / Cursor / 自研 Agent)都能在**毫秒级**完成自动决策。
[](https://www.python.org/)
[](https://fastapi.tiangolo.com/)
[](https://modelcontextprotocol.io/)
[](https://huggingface.co/convaiinnovations/laya)
</div>
---
## 目录
- [这是什么](#这是什么)
- [核心概念:决策契约(Contract)](#核心概念决策契约contract)
- [为什么值得用](#为什么值得用)
- [架构](#架构)
- [快速开始](#快速开始)
- [接入 MCP Agent](#接入-mcp-agent)
- [MCP 工具参考](#mcp-工具参考)
- [Resources 与 Prompts](#resources-与-prompts)
- [内置问题集(Preset)](#内置问题集preset)
- [问题(Question)Schema](#问题question-schema)
- [REST API](#rest-api)
- [命令行 CLI](#命令行-cli)
- [配置项(环境变量)](#配置项环境变量)
- [Docker 部署](#docker-部署)
- [开发与测试](#开发与测试)
- [常见问题 FAQ](#常见问题-faq)
- [许可证](#许可证)
---
## 这是什么
`laya-mcp` 把 [Laya](https://huggingface.co/convaiinnovations/laya)(一个 **System-1** 决策模型族)
包装成一个**常驻服务**,同时对外暴露两种接口:
| 接口 | 地址 | 面向谁 |
| --- | --- | --- |
| **MCP Streamable HTTP** | `http://127.0.0.1:8077/mcp` | 任意 MCP 客户端 / Agent |
| **REST API** | `http://127.0.0.1:8077/v1/...` | 脚本、看板、健康检查、非 MCP 调用方 |
| **OpenAPI 文档** | `http://127.0.0.1:8077/docs` | 人 |
> 它不是「又一个 LLM 接口」。Laya 是**非生成式**的:一次 forward pass 回答**一组结构化问题**,
> 输出的是**校准过的概率分布**,而不是一段文本。这使得它可以在 10~50ms 内做出「该走哪条路」的判断,
> 成本约为调用一次完整 LLM 的千分之一。
### 与传统做法对比
`laya-test.py`(见 [`examples/quickstart_router.py`](examples/quickstart_router.py))演示了最小用法:
加载 Router → 构造问题 → `router.predict()` → 用阈值手写 `if`。
这个工程把它**产品化**了:
| 关注点 | `laya-test.py` | `laya-mcp` |
| --- | --- | --- |
| 模型驻留 | 每次进程启动重新加载 | 常驻单例 + 启动预热(warmup) |
| 阈值策略 | 每个调用方自己写 `if conf >= 0.8` | **决策契约**,由服务统一输出 `grade` |
| 并发 | 单线程阻塞 | 线程池 + 事件循环不阻塞 + 结果缓存 |
| 接入方式 | 只能进程内 Python 调用 | MCP / REST / CLI 三种入口共用同一套逻辑 |
| 可观测性 | `print` | 结构化日志 + 指标(计数 / 延迟分位) + `/healthz` |
| 错误处理 | 直接抛异常 | 统一错误码 + HTTP 状态映射 |
| 配置 | 硬编码 | `LAYAMCP_*` 环境变量 / `.env` |
| Agent 可发现性 | 无 | 工具自描述 + resources 知识库 + prompt 模板 |
---
## 核心概念:决策契约(Contract)
**这是整个项目最重要的设计。** 模型给出的是概率,但 Agent 需要的是**策略**:
「我现在可以直接执行吗?还是必须找人确认?」
原始概率无法直接回答这个问题——两个策略 0.79 / 0.78 时,最高分看起来很高,但**它不是一个决策**。
因此每次调用都会额外返回一个 `contract` 块,把概率翻译成 4 种 **grade**,Agent 直接分支即可:
| `grade` | 含义 | Agent 应该做什么 |
| --- | --- | --- |
| ✅ `auto_execute` | 置信度越过阈值,且与次优有明显差距 | **直接执行**,不要再花 token 重新推理 |
| ⚠️ `escalate` | 高置信但存在近距平局,或问题本身语义上就该由人拍板 | 不要仅凭模型答案行动,走完整 LLM 分析或请人确认 |
| 🛑 `ask_human` | 低置信 + 高风险场景 | **必须转人工** |
| 🔁 `reconsider` | 低置信 + 低风险 | 可以重试:换更精确的 criteria、增加候选、或交给 LLM |
### grade 由三个信号共同决定(而不是单一置信度)
| 信号 | 作用 | 为什么单独看它不行 |
| --- | --- | --- |
| `confidence` | 校准后的置信度 | 候选数量多时 softmax 会被抬高,必须用 Laya 的温度校准值 |
| `margin` | top1 − top2 | 0.79 / 0.78 的两条相邻策略,不管最高分多高都不构成决策 |
| `needs_review` | 模型自带的「需人工复核」头 | 对语义上就要求人拍板的问题,**高置信也依然是 escalate** |
### 契约结构示例
```json
{
"grade": "auto_execute",
"autonomous": true,
"requires_human": false,
"threshold": 0.8,
"margin_threshold": 0.05,
"decisions": {
"selected_strategy": {
"value": "redis_cache",
"confidence": 0.9231,
"margin": 0.41,
"grade": "auto_execute",
"reason": "confidence 0.92 clears 0.80 and beats the runner-up by 0.41",
"requires_human": false
}
},
"grade_counts": { "auto_execute": 1, "escalate": 0, "ask_human": 0, "reconsider": 0 },
"reasons": [],
"summary": "all 1 answers clear the auto-execute bar; proceed",
"routing": { "model": "english", "reason": "English Latin text" }
}
```
> **聚合规则:一个被卡住的问题会卡住整个请求。** 整体 `grade` 取所有问题中最「保守」的那个
> (优先级 `ask_human` > `escalate` > `reconsider` > `auto_execute`),
> 因为对 Agent 而言「部分可执行」比「整体不可执行」更危险。
### Agent 典型分支伪代码
```python
result = mcp.call("decide", state=diff, preset="code_change_risk")
grade = result["contract"]["grade"]
if grade == "auto_execute":
merge_pull_request() # 直接干,零推理成本
elif grade == "escalate":
run_full_llm_review(diff) # 花 token 深挖
elif grade == "ask_human":
notify_reviewer(result["contract"]["reasons"])
else: # reconsider
result = mcp.call("decide", state=diff, preset="code_change_risk",
threshold="critical") # 换阈值 / 换问题重试
```
---
## 为什么值得用
- **大幅降低 Agent 决策成本**:把「要不要做 / 走哪条路」交给 System-1 模型,避免为此唤起完整 LLM 的 CoT。
- **不引入幻觉风险**:非生成式,输出是封闭候选集上的概率,不会「编造」答案。
- **策略集中在服务端**:阈值、margin、风险等级都是**配置**,不用在每个 Agent 里重复实现。
- **对 Agent 友好**:工具自带详细 description 与 `structuredContent`,另有 `laya://guide` 等资源做知识注入。
- **一套逻辑三种入口**:MCP / REST / CLI 共用 `laya_mcp.core`,不会出现行为漂移。
- **生产可用**:健康检查、指标、结构化日志、超时与体积上限、线程安全、优雅降级(模型加载失败时 `/healthz` 返回 `degraded` 而非崩溃)。
---
## 架构
```
┌─────────────────────────────┐
MCP Agent ────────► │ /mcp StreamableHTTP │
(Claude Code / │ (9 tools / 4 resources / │
Codex / Cursor) │ 2 prompts) │
├─────────────────────────────┤
脚本 / 看板 ────────► │ /v1/... REST (FastAPI) │
└──────────────┬──────────────┘
│ 共用同一套实现
┌──────────────▼──────────────┐
│ laya_mcp.core │
│ settings / schema / preset │
│ engine(常驻+池+缓存) │
│ contract(决策契约) / metrics │
└──────────────┬──────────────┘
│
┌──────────────▼──────────────┐
│ laya.Router (System-1) │
│ english / multilingual / │
│ typed-decisions │
└─────────────────────────────┘
```
### 目录结构
```
laya-mcp/
├── src/laya_mcp/
│ ├── core/ # 与传输方式无关的领域层
│ │ ├── settings.py # LAYAMCP_* 配置、阈值等级
│ │ ├── schema.py # 公开问题 schema ↔ Laya 原生格式
│ │ ├── presets.py # 5 个内置问题集
│ │ ├── contract.py # ★ 概率 → grade 的决策契约
│ │ ├── engine.py # 单例 Router + 线程池 + LRU 缓存
│ │ ├── errors.py # 统一错误分类与 HTTP 映射
│ │ └── metrics.py # 计数 / 延迟分位
│ ├── api/ # FastAPI 层
│ │ ├── app.py # create_app(),挂载 /mcp 与 /v1
│ │ ├── routes.py # REST 端点(不含决策逻辑)
│ │ ├── schemas.py # 请求 / 响应模型
│ │ └── dependencies.py # DI:engine / settings / metrics
│ ├── mcp_server/ # MCP 层
│ │ ├── server.py # 工具 / 资源 / Prompt 注册
│ │ └── questions.py # 问题解析(inline / preset / 快捷参数)
│ ├── cli.py # 命令行入口
│ └── logging_config.py # 结构化日志
├── examples/quickstart_router.py # 原始 laya-test.py 用法
├── tests/
└── pyproject.toml
```
### 关键实现说明
- **模型只加载一次**:`DecisionEngine` 进程级单例。启动时预热(`LAYAMCP_WARMUP=true`),
之后每次请求都是热态,毫秒级返回。
- **不阻塞事件循环**:推理跑在有界线程池里(`LAYAMCP_WORKER_THREADS`),
torch 内部会释放 GIL,因此 CPU 上并发也有收益。
- **结果缓存**:对相同 `(state, questions)` 做进程内 LRU 缓存(默认 256 条),
Agent 重试同样的问题几乎零成本;响应中 `cached=true` 可辨别。
- **MCP 挂载方式(容易踩坑)**:**不能**用 `streamable_http_app()` 再 `mount` 到 FastAPI——
Starlette 不会为子应用执行 lifespan,session manager 起不来,`/mcp` 会全部失败。
正确做法是在**本应用**的 lifespan 里进入 session manager:
```python
manager = StreamableHTTPSessionManager(app=server._lowlevel_server, ...)
app.mount("/mcp", StreamableHTTPASGIApp(manager))
# lifespan: async with manager.run(): yield
```
参见 [`src/laya_mcp/api/app.py`](src/laya_mcp/api/app.py) 的模块注释。
- **MCP SDK 版本**:基于 **mcp 2.x**(`from mcp.server.mcpserver import MCPServer`)。
与 1.x 的 `mcp.server.fastmcp.FastMCP` **不兼容**,升级时注意导入路径变化。
---
## 快速开始
### 环境要求
- Python **3.13+**
- [uv](https://docs.astral.sh/uv/)(推荐)
- 磁盘约 **3 GB**(三个 checkpoint 合计约 2.2 GB)
- CPU 即可运行;有 CUDA / Apple MPS 会自动加速
- 首次运行需要联网从 HuggingFace 下载权重,之后可完全离线
### 安装
```bash
git clone <your-repo-url> laya-mcp
cd laya-mcp
uv sync # 创建 .venv 并安装依赖
```
### 自检
```bash
uv run laya-mcp doctor
```
输出示例:
```
laya-mcp 0.2.0
python: 3.13.x
settings: models=['english', 'multilingual'] device=auto port=8077
laya: 0.3.5 -> .../laya/__init__.py
torch: 2.x.x cuda=False
HF 缓存: 2.20 GB,包含 laya 检查点: True
fastapi: ok
uvicorn: ok
mcp: ok
pydantic_settings: ok
结论: 环境可用
```
### 启动服务
```bash
uv run laya-mcp serve
```
```
INFO laya_mcp.engine loading laya router models=['english','multilingual'] device=cpu
INFO laya_mcp.engine laya router ready loaded=['english','multilingual']
INFO laya_mcp.api engine ready in 67.8s
INFO mcp.server... StreamableHTTP session manager started
Uvicorn running on http://127.0.0.1:8077
```
- 服务首页:<http://127.0.0.1:8077/>
- OpenAPI 文档:<http://127.0.0.1:8077/docs>
- MCP 端点:`http://127.0.0.1:8077/mcp`
> ⏳ **首次启动约 60~90 秒**(在 CPU 上加载两个 checkpoint)。之后所有请求都是热态,
> 单次决策通常 **10~50ms**。
### 第一条请求
```bash
curl -s http://127.0.0.1:8077/v1/decide \
-H 'content-type: application/json' \
-d '{
"state": "Fix a typo in README, one file, no interface change",
"preset": "code_change_risk"
}' | python -m json.tool
```
或者完全不启服务,直接一次性决策:
```bash
uv run laya-mcp ask \
--state "Fix a typo in README, one file, no interface change" \
--preset code_change_risk
```
---
## 接入 MCP Agent
服务启动后,**任何支持 MCP 的客户端**都可以直接接入。关键信息只有一条:
```
MCP Streamable HTTP 端点:http://127.0.0.1:8077/mcp
```
### Claude Code
```bash
claude mcp add --transport http laya http://127.0.0.1:8077/mcp
```
### Codex / Cursor / 通用 `mcp.json`
```json
{
"mcpServers": {
"laya": {
"type": "streamable-http",
"url": "http://127.0.0.1:8077/mcp"
}
}
}
```
### 只给单个 Agent 用(stdio,无 HTTP)
不想起 HTTP 服务时,可以用 stdio 传输,由客户端自己拉起进程:
```json
{
"mcpServers": {
"laya": {
"command": "uv",
"args": ["run", "--directory", "/abs/path/to/laya-mcp", "laya-mcp", "serve", "--transport", "stdio"]
}
}
}
```
> stdio 模式下进程由客户端按需拉起,**建议启动后先让 Agent 调一次 `preload_warmup`**,
> 否则第一次决策会等待模型加载。HTTP 模式则无需关心——服务启动时已预热。
### 让 Agent「自动决策」的推荐接入方式
1. **在系统提示里注入用法**。服务已经在 MCP `instructions` 里写了使用指南,
另外可以让 Agent 读取 `laya://guide` 资源获取完整的问题编写规范。
2. **强制 Agent 先查契约**。约定:任何有副作用的动作前,先调用 `decide` / `should_escalate`,
并**只在 `contract.grade == "auto_execute"` 时**直接执行。
3. **按风险等级传 `threshold`**。删库、发钱、改线上配置这类动作,传 `"high"` 或 `"critical"`。
4. **用 `preset` 起步,再按需内联问题**。preset 与内联 `questions` 会**合并到同一次前向传播**,
边际成本几乎为零。
---
## MCP 工具参考
共 **9 个工具**,全部返回结构化结果(文本 + `structuredContent` 字段同名)。
| 工具 | 用途 | 是否加载模型 | 典型耗时 |
| --- | --- | --- | --- |
| `decide` | **主力工具**:对 `state` 回答一组带类型的问题,返回答案 + 契约 | 是 | 10~50ms |
| `decide_yes_no` | 最省成本:单个是非问题 + 契约 | 是 | ~10ms |
| `decide_shortlist` | 候选标签极多(几十~上百)时,先 embedding 粗筛 top-k 再决策 | 是 | 20~80ms |
| `route_request` | 只做路由:判断语言 / 脚本 / 工作流,返回该用哪个 checkpoint 及原因 | **否** | <1ms |
| `list_presets` | 列出全部内置问题集及其 question id | 否 | <1ms |
| `describe_questions` | 回显并校验一组问题(id、kind、候选、等级),不推理 | 否 | <1ms |
| `should_escalate` | 一站式:给定动作,直接回答「是否需要人工介入」 | 是 | 10~50ms |
| `server_status` | 就绪状态、已加载 checkpoint、缓存条数、延迟分位 | 否 | <1ms |
| `preload_warmup` | 强制加载 checkpoint(仅 stdio 或关闭预热时需要) | 是(慢) | 30~90s |
### 调用顺序建议(先便宜后昂贵)
```
route_request < list_presets / describe_questions < decide_yes_no < decide < decide_shortlist
(<1ms) (<1ms) (~10ms) (10~50ms) (20~80ms)
```
### `decide` 参数速查
| 参数 | 类型 | 说明 |
| --- | --- | --- |
| `state` | string \| object \| array | **必填**。模型真正读到的上下文:代码 / diff / 工单 / trace / 工具返回值。**放原始材料,不要放摘要**,摘要会掉准确率 |
| `questions` | object \| array | 内联问题集,见 [Question Schema](#问题question-schema) |
| `preset` | string | 内置问题集名;可与 `questions` 合并 |
| `threshold` | number \| string | `auto_execute` 所需置信度。数字 ∈ [0,1],或等级 `trivial/low/normal/high/critical` |
| `model` | string | 强制 checkpoint:`english` / `multilingual` / `typed-decisions` |
| `lang` | string | 提示 `state` 的语言(如 `zh` / `en`),跳过语言检测 |
| `task` | string | 强制使用 `typed-decisions` 的四个工作流之一 |
| `use_cache` | bool | 是否复用相同请求的缓存结果,默认 `true` |
### 返回值结构
```jsonc
{
"contract": { /* 见上文「决策契约」 */ },
"grade": "auto_execute", // 整体 grade,Agent 只需看这个
"autonomous": true, // 是否可完全自主执行
"requires_human": false,
"summary": "all 5 answers clear the auto-execute bar; proceed",
"answers": {
"blast_radius": {
"kind": "choice",
"value": "single_file",
"confidence": 0.9642,
"margin": 0.8713,
"grade": "auto_execute",
"reason": "confidence 0.96 clears 0.80 and beats the runner-up by 0.87",
"requires_human": false,
"probabilities": { "single_file": 0.9642, "module": 0.0929, "...": 0.0 },
"raw": { /* Laya 原始返回,未加工 */ }
}
},
"routing": { "model": "english", "reason": "English Latin text", "repo": "convaiinnovations/laya" },
"latency_ms": 31.4,
"cached": false,
"usage": {}
}
```
---
## Resources 与 Prompts
除了工具,服务还暴露 MCP **Resources**(可被 Agent 当知识库读取)与 **Prompts**(可复用模板)。
### Resources
| URI | 内容 |
| --- | --- |
| `laya://guide` | **问题编写指南**、kind 说明、grade 语义(强烈建议 Agent 读取) |
| `laya://schema` | `decide` / `describe_questions` 接受的问题 JSON Schema |
| `laya://presets` | 全部内置问题集(JSON) |
| `laya://status` | 实时就绪状态与延迟,等同 `server_status` |
### Prompts
| 名称 | 用途 |
| --- | --- |
| `decide_before_acting` | 把「打算执行的动作」转成一组问题集的填空模板 |
| `triage_message` | 对一条用户消息做分诊(意图 / 紧急度 / 情绪 / 流失风险) |
---
## 内置问题集(Preset)
Preset 是**问题的骨架**,不是答案。它们覆盖了 Agent 最常遇到的五类场景:
| 名称 | 问题数 | 适用场景 | 包含的 question id |
| --- | --- | --- | --- |
| `code_change_risk` | 5 | 代码改动 **前置风险评审**:爆炸半径、可回滚性、数据影响、测试覆盖、是否需人工 | `blast_radius`, `reversible`, `data_impact`, `test_coverage`, `needs_human_review` |
| `support_triage` | 4 | **客服工单分诊**:意图、紧急度、情绪、流失风险 | `intent`, `is_urgent`, `frustration`, `churn_risk` |
| `input_guard` | 5 | **不可信输入的护栏**:越狱、提示注入、敏感数据、危害程度、话题 | `jailbreak`, `prompt_injection`, `sensitive_data`, `harm_severity`, `topic` |
| `routing` | 4 | **模型/算力路由**:难度、领域、是否需要工具、敏感性 | `difficulty`, `domain`, `needs_tools`, `is_sensitive` |
| `typed_decisions_agent_trace` | 5 | **Agent trace 可观测性**(与 `typed-decisions` checkpoint 的工作流对齐) | `action`, `needs_review`, `outcome`, `risk`, `urgency` |
```bash
# 查看全部 preset
uv run laya-mcp presets
# 查看单个 preset 的完整问题定义
curl -s http://127.0.0.1:8077/v1/presets/code_change_risk | python -m json.tool
```
**preset + 自定义问题可以合并**,一次前向传播全部回答:
```json
{
"state": "<diff 或工单>",
"preset": "code_change_risk",
"questions": {
"target_release": {
"kind": "choice",
"instructions": "这个改动更适合进哪个版本?",
"criteria": {
"hotfix": "紧急修复,必须立刻发",
"next_minor": "下个小版本即可",
"backlog": "不急,排入待办"
}
}
}
}
```
---
## 问题(Question)Schema
Laya 原生词汇是 `choice` / `score` / `noul`(且使用 `t` / `ins` / `crit` 短键)。
本工程定义了一套**稳定、自描述**的公开 schema,调用方不需要了解底层格式:
```jsonc
{
"question_id": {
"kind": "choice", // choice | score | yes_no
"instructions": "选哪个方案?", // 给模型的指令
"criteria": { // 见下方「三种 kind」
"redis_cache": "适合读多写少、容忍秒级延迟",
"db_index": "适合查询条件固定、数据量中等"
},
"weight": 1.0, // 可选,建议性权重
"description": "..." // 可选,仅在罗列时展示
}
}
```
### 三种 `kind`
| `kind` | 对应 Laya 类型 | `criteria` 格式 | 说明 |
| --- | --- | --- | --- |
| `choice` | `choice` | `{标签: 何时选它}` | N 选 1。**每个候选的适用条件要写具体、可区分** |
| `score` | `score` | `["等级0", "等级1", ...]` | 有序等级列表,返回等级下标 |
| `yes_no` | `noul` | `{"true": "应该成立时的陈述"}` | 是非问题。**陈述句要正向表述**(true 表示「可以继续」) |
### 编写要点(直接影响准确率)
1. **一个问题只问一件事**。不要问「这个改动安全吗」,而是拆成爆炸半径 / 可回滚 / 数据影响。
2. **候选之间必须互斥且穷尽**。留一个 `other` 兜底往往比硬选更好。
3. **criteria 写判定条件,不写形容词**。✅「涉及共享 schema,会被其他团队消费」 ❌「影响很大」。
4. **`state` 放原始材料**,代码、diff、完整工单原文,不要放你自己的总结。
5. **问题数量 2~6 个最佳**(上限由 `LAYAMCP_MAX_QUESTIONS` 控制,默认 32),
每个问题的边际成本极低——多问一个几乎不增加耗时。
6. **不要用 Laya 做生成任务**。它只做分类/打分,不是写作模型。
### 校验问题集
写好后先让服务回显校验,避免把无效问题发到模型:
```bash
curl -s http://127.0.0.1:8077/v1/questions/describe \
-H 'content-type: application/json' \
-d '{"preset": "input_guard"}' | python -m json.tool
```
---
## REST API
Base path:`/v1`。完整交互式文档见 `/docs`。
### 决策
| 方法 | 路径 | 说明 |
| --- | --- | --- |
| `POST` | `/v1/decide` | 回答一组带类型的问题(等同 MCP `decide`) |
| `POST` | `/v1/decide/yes-no` | 单个是非问题 |
| `POST` | `/v1/decide/shortlist` | 大规模候选:embedding 粗筛 + 决策 |
| `POST` | `/v1/route` | 只做路由,不推理(微秒级) |
### 目录 / 运维
| 方法 | 路径 | 说明 |
| --- | --- | --- |
| `GET` | `/v1/presets` | 全部内置问题集(含问题定义) |
| `GET` | `/v1/presets/{name}` | 单个问题集(规范化后的最终形态) |
| `POST` | `/v1/questions/describe` | 校验并回显一组问题 |
| `GET` | `/v1/models` | 可用 checkpoint 及加载状态 |
| `GET` | `/v1/healthz` | 存活检查(模型加载失败返回 200 + `degraded`) |
| `GET` | `/v1/status` | 引擎状态、阈值、缓存、延迟分位 |
| `GET` | `/v1/metrics` | 原始计数与延迟数据(供 Prometheus 等抓取) |
| `GET` | `/v1/version` | 服务版本与 laya 版本 |
### 示例
```bash
# 1) 是非问题:这个动作能直接执行吗?
curl -s http://127.0.0.1:8077/v1/decide/yes-no \
-H 'content-type: application/json' \
-d '{
"state": {"action": "DROP TABLE users", "env": "production"},
"statement": "this action is safe to run without human review",
"threshold": "critical"
}' | python -m json.tool
# 2) 中文工单分诊(自动路由到 multilingual checkpoint)
curl -s http://127.0.0.1:8077/v1/decide \
-H 'content-type: application/json' \
-d '{"state": "客户反馈发票被重复扣款,语气很急", "preset": "support_triage"}' \
| python -m json.tool
# 3) 只路由,不推理
curl -s http://127.0.0.1:8077/v1/route \
-H 'content-type: application/json' \
-d '{"state": "给我翻译一下这段中文"}' | python -m json.tool
# {"model":"multilingual","reason":"non-Latin script (han, 100% of letters); ..."}
```
### 统一错误格式
所有错误都是同一形状,并带 `request_id` 便于排查:
```json
{
"error": "invalid_question",
"message": "at most 32 questions per call",
"details": { "count": 40, "limit": 32 },
"request_id": "0f3a9c1b2d4e5f60"
}
```
---
## 命令行 CLI
```bash
uv run laya-mcp --help
```
| 命令 | 说明 |
| --- | --- |
| `laya-mcp serve` | 启动服务(默认 FastAPI + MCP Streamable HTTP) |
| `laya-mcp serve --transport stdio` | 仅提供 MCP,走标准输入输出,适合本地 Agent |
| `laya-mcp ask --state ... --choices ...` | **一次性决策**,不启服务(等价于 `laya-test.py` 的用法) |
| `laya-mcp presets` | 列出内置问题集 |
| `laya-mcp tools` | 列出 MCP 工具(**不加载模型**) |
| `laya-mcp doctor` | 检查环境、依赖与 checkpoint 缓存 |
### `serve` 常用参数
```bash
uv run laya-mcp serve \
--host 0.0.0.0 \
--port 8077 \
--models english,multilingual,typed-decisions \
--device auto \
--log-level INFO
```
| 参数 | 说明 |
| --- | --- |
| `--transport` | `streamable-http`(默认,含 REST)/ `stdio`(仅 MCP)/ `sse` |
| `--host` / `--port` | 监听地址与端口(默认 `127.0.0.1:8077`) |
| `--models` | 常驻 checkpoint,逗号分隔,或 `all` |
| `--device` | `auto` / `cpu` / `cuda` / `mps` |
| `--reload` | 开发热重载(仅 HTTP) |
| `--no-warmup` | 启动时不加载模型(首次请求会变慢) |
### `ask` 示例
```bash
# N 选 1(--choices 是快捷写法)
uv run laya-mcp ask \
--state "高并发下数据库查询变慢,Query 全表扫描,QPS 5000/s" \
--instructions "根据性能瓶颈选择最合适的优化方案" \
--choices "redis_cache,db_index,read_write_split,search_engine"
# 读取文件作为 state(@ 前缀)
uv run laya-mcp ask --state @./diff.patch --preset code_change_risk
# 输出完整 JSON(含契约)
uv run laya-mcp ask --state "..." --preset input_guard --json
# 是非问题
uv run laya-mcp ask --state "<发布计划>" \
--statement "this can ship without human approval"
```
`ask` 的可读输出示例:
```
5/5 answers fail the auto-execute bar (...): ask a human before acting
blast_radius: single_file (choice, confidence 0.64, margin 0.82, ask_human)
needs_human_review: True (yes_no, confidence 0.60, margin 0.21, ask_human)
checkpoint: english - English Latin text
```
---
## 配置项(环境变量)
所有配置项前缀为 **`LAYAMCP_`**,也可写在项目根目录的 `.env` 里
(见 [`.env.example`](.env.example))。
### 服务
| 变量 | 默认值 | 说明 |
| --- | --- | --- |
| `LAYAMCP_HOST` | `127.0.0.1` | 监听地址 |
| `LAYAMCP_PORT` | `8077` | 监听端口 |
| `LAYAMCP_TRANSPORT` | `streamable-http` | `streamable-http` / `stdio` / `sse` |
| `LAYAMCP_MCP_PATH` | `/mcp` | MCP 挂载路径 |
| `LAYAMCP_API_PREFIX` | `/v1` | REST 前缀 |
| `LAYAMCP_DOCS_ENABLED` | `true` | 是否开启 `/docs` |
### 推理
| 变量 | 默认值 | 说明 |
| --- | --- | --- |
| `LAYAMCP_MODELS` | `english,multilingual` | 常驻 checkpoint,逗号分隔或 `all` |
| `LAYAMCP_DEVICE` | `auto` | `auto` / `cpu` / `cuda` / `mps`(auto 优先 CUDA → MPS → CPU) |
| `LAYAMCP_DEFAULT_MODEL` | `english` | 默认 checkpoint |
| `LAYAMCP_AUTO_TASK_DETECTION` | `true` | 自动识别语言/脚本/工作流并路由 |
| `LAYAMCP_WARMUP` | `true` | 启动时预热加载模型 |
| `LAYAMCP_WORKER_THREADS` | `2` | 推理线程池大小(负载高时调大) |
| `LAYAMCP_HF_TOKEN` | 无 | 私有镜像或受限下载时使用 |
### 策略(决策契约的阈值)
| 变量 | 默认值 | 说明 |
| --- | --- | --- |
| `LAYAMCP_CONFIDENCE_THRESHOLD` | `0.80` | 达到此置信度才可能判为 `auto_execute` |
| `LAYAMCP_MARGIN_THRESHOLD` | `0.05` | top1−top2 低于此值视为「近距平局」→ `escalate` |
| `LAYAMCP_MAX_STATE_CHARS` | `120000` | `state` 最大字符数 |
| `LAYAMCP_MAX_QUESTIONS` | `32` | 单次调用最大问题数 |
| `LAYAMCP_MAX_CHOICES` | `60` | 单个 choice 问题最大候选数 |
### 日志与指标
| 变量 | 默认值 | 说明 |
| --- | --- | --- |
| `LAYAMCP_LOG_LEVEL` | `INFO` | `DEBUG` / `INFO` / `WARNING` / `ERROR` / `CRITICAL` |
| `LAYAMCP_LOG_JSON` | `true` | 结构化 JSON 日志(接入 ELK / Loki 时保留) |
| `LAYAMCP_METRICS_ENABLED` | `true` | 是否采集指标 |
| `LAYAMCP_REQUEST_ID_HEADER` | `x-request-id` | 请求 id 头名称 |
### 置信度等级
`threshold` 参数/配置支持用**名字**代替数字,便于按风险分级:
| 等级 | 数值 | 建议场景 |
| --- | --- | --- |
| `trivial` | 0.60 | 无副作用的分类、打标签 |
| `low` | 0.70 | 内部草稿、低风险建议 |
| `normal` | **0.80**(默认) | 常规 Agent 决策 |
| `high` | 0.90 | 生产变更、影响用户的操作 |
| `critical` | 0.95 | 删数据、动钱、安全相关 |
---
## Docker 部署
```dockerfile
# 参考 Dockerfile
FROM ghcr.io/astral-sh/uv:python3.13-bookworm-slim
WORKDIR /app
ENV UV_COMPILE_BYTECODE=1 \
UV_LINK_MODE=copy \
LAYAMCP_HOST=0.0.0.0 \
LAYAMCP_PORT=8077 \
HF_HOME=/models
COPY pyproject.toml uv.lock README.md ./
RUN uv sync --frozen --no-dev --no-install-project
COPY src ./src
RUN uv sync --frozen --no-dev
EXPOSE 8077
CMD ["uv", "run", "--no-dev", "laya-mcp", "serve"]
```
```bash
docker build -t laya-mcp .
docker run --rm -p 8077:8077 -v laya-models:/models \
-e LAYAMCP_MODELS=english,multilingual \
laya-mcp
```
> 建议把 `/models` 挂成 volume:首次下载约 2.2 GB,重建镜像不必重下。
> 生产环境请在服务前置反向代理并加鉴权——MCP 端点本身无内建认证。
---
## 开发与测试
```bash
uv sync
uv run laya-mcp doctor # 环境自检
uv run laya-mcp tools # 列出工具(不加载模型,适合快速验证)
uv run pytest -q # 运行测试
```
### 不加载模型做冒烟测试
```bash
# 仅启动 app,不预热(秒级返回)
LAYAMCP_WARMUP=false uv run python -c "
from fastapi.testclient import TestClient
from laya_mcp.api.app import create_app
with TestClient(create_app()) as c:
print(c.get('/').json())
print(c.get('/v1/healthz').json())
"
```
### 扩展新工具
1. 在 `src/laya_mcp/core/` 里实现**与传输无关**的逻辑。
2. 在 `src/laya_mcp/mcp_server/server.py` 里注册 `@server.tool(...)`,返回 Pydantic 模型。
3. 若要暴露 REST,在 `src/laya_mcp/api/routes.py` 加端点,**复用同一函数**,不要重写逻辑。
4. 补充 `laya://guide` 里的说明,让 Agent 知道何时该用这个新工具。
---
## 常见问题 FAQ
<details>
<summary><b>首次启动很慢,正常吗?</b></summary>
正常。CPU 上加载 `english` + `multilingual` 两个 checkpoint 约需 60~90 秒。
这是**一次性**成本——服务预热后单次决策仅 10~50ms。
临时验证接口、不想等加载:`--no-warmup`(或 `LAYAMCP_WARMUP=false`),
此时首次决策会变慢。只验证工具列表可用 `uv run laya-mcp tools`(完全不加载模型)。
</details>
<details>
<summary><b>中文/日文输入返回结果很奇怪?</b></summary>
检查 `routing.model` 是否为 `multilingual`。Laya 的 `english` checkpoint 是
ModernBERT-large(512 token,只懂英文),中文会被**自动路由**到 `multilingual`(mmBERT-base,1024 token)。
若自动路由不符合预期,可显式指定:`model="multilingual"` 或 `lang="zh"`。
注意 `multilingual` 上下文更长(1024)但推理略慢,纯英文场景用 `english` 更快更准。
</details>
<details>
<summary><b>置信度看起来偏高 / 与直觉不符?</b></summary>
Laya 返回的是**校准后**的置信度,但候选数量多时原始 softmax 仍会被抬高,
`contract.py` 里的 `confidence_from_probs` 已经做了校正。
另外注意启动时的告警:
```
laya: this checkpoint ships temperatures outside [0.5, 5] ... Treat confidence
from the affected buckets as uncalibrated.
```
命中这类 bucket 时,**不要只看 confidence**,务必参考 `margin` 与整体 `grade`——
这正是契约存在的意义。
</details>
<details>
<summary><b>`/mcp` 全部 404 / 连接失败?</b></summary>
两个常见原因:
1. **挂载方式错误**。不能用 `streamable_http_app()` 再 `mount` 到 FastAPI(子应用 lifespan 不会执行,
session manager 起不来)。请直接用 `create_app()`。
2. **客户端连接方式不对**。MCP 是**有状态**的 Streamable HTTP,客户端要用 `streamable-http` 传输类型
指向 `http://<host>:<port>/mcp`;用 SSE 或旧版 HTTP 传输连不上。
若前置了反向代理,请确保**允许流式响应**(关闭 buffering、开启 chunked、放行 `mcp-session-id` 头)。
</details>
<details>
<summary><b>可以完全离线运行吗?</b></summary>
可以。权重下载一次后会缓存在 `HF_HOME`(默认 `~/.cache/huggingface`)。
用 `uv run laya-mcp doctor` 确认 `包含 laya 检查点: True`,之后设置 `HF_HUB_OFFLINE=1` 即可离线。
</details>
<details>
<summary><b>多个 Agent 同时调用会不会打爆?</b></summary>
不会。模型是进程级单例,推理跑在有界线程池里,相同请求命中 LRU 缓存。
横向扩展只需多起几个进程/容器(每个进程会各自加载模型,注意 CPU/显存与内存占用)。
</details>
<details>
<summary><b>和直接调用 LLM 相比,什么时候该用它?</b></summary>
| 场景 | 用 Laya | 用 LLM |
| --- | --- | --- |
| 「走哪条路 / 是不是 X / 严重程度几级」 | ✅ | ❌ 太贵太慢 |
| 需要生成文本、写代码、长链推理 | ❌ | ✅ |
| 高频、对延迟敏感(<100ms) | ✅ | ❌ |
| 候选集封闭、要求可复现 | ✅ | ⚠️ 不稳定 |
实践上两者是**互补**关系:用 Laya 做**前置判断与分流**,只在 `grade != auto_execute` 时才唤起 LLM。
</details>
<details>
<summary><b>如何自定义问题集?(不写代码)</b></summary>
直接把 `questions` 内联传给 `decide` 即可,无需改代码:
```json
{
"state": "...",
"questions": {
"priority": {
"kind": "choice",
"instructions": "这条需求应该排什么优先级?",
"criteria": {
"p0": "线上故障,立刻处理",
"p1": "本周内必须完成",
"p2": "可以排入下个迭代"
}
}
}
}
```
需要长期复用时,在 [`src/laya_mcp/core/presets.py`](src/laya_mcp/core/presets.py) 的 `PRESETS` 中注册一个新条目。
</details>
<details>
<summary><b>怎么监控服务健康?</b></summary>
- `GET /v1/healthz` — 存活(模型失败也返回 200,`status=degraded`)
- `GET /v1/status` — 就绪、已加载 checkpoint、缓存、延迟分位
- `GET /v1/metrics` — 原始计数与延迟,可直接被抓取
- 所有响应带 `x-request-id` 与 `x-response-time-ms` 头
</details>
---
## 参考
- [Laya 模型(HuggingFace)](https://huggingface.co/convaiinnovations/laya)
- [Model Context Protocol 规范](https://modelcontextprotocol.io/)
- [FastAPI 文档](https://fastapi.tiangolo.com/)
- 原始最小示例:[`examples/quickstart_router.py`](examples/quickstart_router.py)
---
## 许可证
见仓库根目录 `LICENSE`。使用 Laya 权重时请同时遵守其 HuggingFace 页面上的许可条款。
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues