# 七层协议栈详解

> 本文对应知识库条目 [EG-101]、[EG-001]、[EG-301]。机读版本：`GET /api/context?topic=protocol&budget=2000`

AI Agent 访问一个普通网站时，真正消耗预算的不是「读不懂」，而是**噪声**：导航栏、脚本标签、CSS 类名、埋点属性、无限滚动的骨架。本站首页实测：9,720 token 的 HTML 里只有 24% 是能支撑断言的事实，而不执行脚本的 Agent 只能拿到 2,372 token [EG-301]。

Evergreen 的做法不是「再写一个 AI 专用站」，而是在**同一个站点**上叠加七层机器可读端点，让 Agent 按自己的预算选择进入的层。

下表右列全部是本仓库的**实测值**（revision 2026-09-02，`_measure.ps1` 可逐行复现，token 公式 `CJK×1.0 + 其他×0.28`）：

| 层 | 端点 | 解决的问题 | 实测体积 |
|---|---|---|---|
| L1 | `/llms.txt` | Agent 进站第一份目录 | 1,904 token |
| L1' | `/llms-full.txt` | 需要全局语境时一次性加载 | 6,369 token |
| L2 | `<page>.html.md` | 去 HTML 噪声、补全 JS 渲染内容 | 8,572 token（HTML 的 88%，密度 0.24→0.86） |
| L3 | `/context.json`、`/api/context` | 结构化 + 按 token 预算裁剪 | budget=2000 时装载 1,888 token |
| L4 | `/.well-known/agent-card.json` | 能力自动发现，无需人工提示 | 1,176 token |
| L5 | `/mcp` | 让 Agent 以工具形式按需下钻 | search_context 1,142 / get_entry 231 |
| L6 | JSON-LD `@graph` | 实体与关系消歧 | 内嵌于 HTML（已计入 9,720） |
| L7 | `/robots.txt`、`/ai.txt`、`/ai-policy.json` | 准入与授权声明 | 554 + 619 + 688 = 1,861 token |

L5 四个工具的完整载荷：`search_context` 1,142、`get_prompt_pack` 671、`get_metrics` 291、`get_entry` 231 token；`tools/list` 的 schema 开销 422 token。量的是 `content[0].text`，JSON-RPC 包装不计——那才是真正进模型上下文的部分。

---

## L1 · llms.txt —— 目录，不是内容

**规范来源**：llms.txt v2（llmstxt.org）。

**它是什么**：一份放在站点根目录的 Markdown 文件，用最小体积告诉 Agent「这个站有什么、按什么优先级读、读的时候要遵守什么规则」。

**硬性格式要求**：

```markdown
# 站点名            ← 唯一必需元素，必须是 H1

> 一段 blockquote 摘要   ← 强烈建议，Agent 常只读这一段就决定去留

零或多个普通段落        ← 补充说明，不带标题

## 分节标题           ← H2 分节
- [显示名](/路径): 一句话说明   ← file list，冒号后的说明是关键
```

**两个约定俗成的规则**：

1. 名为 `Optional` 的 H2 段落，在上下文极紧时**可以整段跳过**。本站把 JSON Schema、参考实现说明、变更日志放在这里。
2. 链接一律用**站点绝对路径**（`/docs/x.md`），不要写完整 URL——否则 Agent 在镜像站上会把请求打到错误的域。

**为什么有效**：Agent 拿到 llms.txt 后不需要爬全站就能建立站点地图。本站实测：无 llms.txt 时平均 4.2 次 GET 才拿到可用上下文，有 llms.txt 后降到 1.3 次 [EG-003]。

**踩坑清单**：

- ❌ 把整篇文档塞进 llms.txt。它应该是**目录**，超过 1500 token 就该拆分到 `llms-full.txt`。
- ❌ 用相对链接（`docs/x.md`）。Agent 拼接 base URL 时容易出错。
- ❌ 只写链接不写说明。`- [文档](/docs/a.md)` 对 Agent 几乎没有信息量，冒号后的那一句才是决策依据。
- ❌ 忘记同时提供 `llms-full.txt`。有些 Agent 会直接找它，找不到就退回去爬 HTML。

**本站的下发细节**：`llms.txt` 与 `llms-full.txt` 的 `Content-Type` 是 `text/markdown; charset=utf-8`，不是 `text/plain`——因为它们的正文就是 Markdown，类型标注错误会让部分 Agent 走纯文本分支而不做结构化解析。

---

## L2 · 页面 Markdown 镜像 —— 同一内容，两种形态

每一个面向人的 HTML 页面，都应该有一个同内容的 `.md` 镜像。命名两种都合规：

- `page.html.md`（推荐，明确表示「这是 page.html 的 md 版」）
- `page.md`

**镜像不一定更小——本站的镜像就比 HTML 只差 12%**（8,572 / 9,720 = 88%）。原因是本站首页的 15 条知识库事实由 `app.js` 运行时渲染，静态 HTML 里一条也没有，所以镜像必须是**补全版**。对纯静态站点，镜像通常能小到 HTML 的 20%～40%。不要把「更小」当作 L2 的验收指标，**可引用密度**才是：本站从 0.24 提到 0.86。

**如何让 Agent 发现它**，三种方式，本站三种都用了：

```html
<!-- 1. HTML head 里声明 -->
<link rel="alternate" type="text/markdown" href="/index.html.md">
<link rel="describedby" href="/llms.txt">
```

```
# 2. HTTP Link 响应头（本站对 .html 请求下发）
Link: </index.html.md>; rel="alternate"; type="text/markdown", </llms.txt>; rel="describedby"
```

```markdown
# 3. llms.txt 的 file list 里直接列出
- [首页 Markdown 镜像](/index.html.md): 与首页同内容的纯净版本，本站实测 8,572 token（HTML 的 88%），可引用密度从 24% 升到 86%
```

**镜像里应该删掉什么**：导航、页脚、装饰性 SVG、动画容器、表单、cookie 横幅、评论区、"相关推荐"。
**必须保留什么**：标题层级、正文段落、表格、代码块、所有事实性数字、指向其他 `.md` 的链接。

**关键纪律**：镜像必须与 HTML **同源生成**。手写两份一定会漂移，漂移之后 Agent 读到的是过期事实，比没有镜像更糟。本站的做法是从 `context.json` 单一数据源渲染两侧。

---

## L3 · context.json 与 /api/context —— 预算化的结构化事实

这是 Evergreen 收益最大的一层，也是与「静态 llms.txt」方案的本质区别。

### 静态全量：`/context.json`

```jsonc
{
  "version": "1.2.0",
  "revision": "2026-09-02",
  "license": "CC-BY-4.0",
  "site": { "name": "...", "canonicalUrl": "...", "rateLimit": { "requestsPerMinute": 120 } },
  "categories": [ { "id": "fact", "label": "站点事实", "description": "..." } ],
  "entries": [
    {
      "id": "EG-001",              // 稳定不变，内容更新也不复用旧 ID
      "category": "fact",
      "title": "Evergreen 是什么",
      "summary": "一句话摘要，Agent 常在只读 summary 的情况下做筛选",
      "body": "完整正文，纯文本，可含换行",
      "tags": ["product", "overview"],
      "confidence": 0.99,          // 0~1，低于 0.80 须提示不确定
      "tokens": 148,               // 预算装箱的依据
      "updated": "2026-08-28"      // ISO 日期，超过 12 个月须提示可能过期
    }
  ]
}
```

### 动态查询：`GET /api/context`

| 参数 | 类型 | 默认 | 说明 |
|---|---|---|---|
| `topic` | string | 空 | 关键词。中英文均可，中文须百分号转义为 UTF-8 |
| `budget` | int | 2000 | token 上限，服务端 clamp 到 `[200, 8000]` |
| `category` | string | 空 | `fact` / `protocol` / `rule` / `principle` / `workflow` |
| `lang` | string | `auto` | 预留，当前不改变结果 |

**响应契约**：

```jsonc
{
  "query": { "topic": "幻觉 引用", "budget": 1200, "category": null },
  "knowledgeVersion": "1.2.0",
  "revision": "2026-09-02",
  "usedTokens": 894,
  "budget": 1200,
  "truncated": false,        // ← 最重要的字段
  "entryCount": 5,
  "entries": [ { "id": "...", "score": 5.973, "relevance": 1, "...": "..." } ],
  "citations": [ { "marker": "[EG-201]", "entryId": "EG-201", "url": "/context.json#EG-201", "text": "来源：...", "confidence": 0.97 } ],
  "rules": [ "每个事实性断言句末标注条目 ID，格式 [EG-xxx]。" ],
  "generatedAt": "2026-09-02T01:06:10Z",
  "source": "api/context"
}
```

`truncated: true` 是一个**明确信号**：预算不足以装下全部相关条目。Agent 的正确反应是提高 `budget` 或用 `topic` 收窄，而不是基于残缺上下文作答 [EG-202]。

### 检索算法（前端与后端同构）

同一套算法在 `assets/app.js` 与 `serve.ps1` 里各实现一次，**行为完全一致**。这样静态托管（没有后端）时前端自动回退到本地实现，交互不降级。

**1. 分词**

```
拉丁词：/[a-z0-9][a-z0-9_\-./]+/g     （小写化后匹配）
CJK：  /[\u3400-\u9FFF]+/g 连续串 → 切二元组（bigram）
停用词过滤：的/了/是/在/the/a/of/...
```

CJK 用二元组而非分词器，是为了零依赖。二元组召回率高、精度略低，但在十几到几百条的知识库规模下足够。

**2. 加权打分**

对每个 term `t`：

| 命中位置 | 加分 |
|---|---|
| `id` | +8 |
| `title` | +5 |
| `tags` | +4 |
| `summary` | +3 |
| `body` | +min(出现次数, 6) |

```
score = rawScore × (0.85 + confidence × 0.15)
```

保留 3 位小数。置信度只做**轻微**加权（最多 ±15%），避免高置信度的泛泛条目压过低置信度的精准命中。

**3. 无命中退化**

若所有 term 都未命中任何条目，则 `score = confidence × 0.1`，`relevance = 0.17`。仍然返回内容——**空响应对 Agent 是灾难**，它无法区分「没有相关知识」和「查询写错了」。

**4. 排序**：`score` 降序，同分按 `id` 升序（保证结果稳定可缓存）。

**5. 贪心装箱**

```
cap = clamp(budget, 200, 8000)
used = 0
for entry in sorted:
    if used + entry.tokens <= cap:
        装载; used += entry.tokens
    else:
        truncated = true   # 继续尝试后面更小的条目，不 break
```

注意是「继续尝试」而非 `break`：一个小条目可能正好塞进剩余空间，这提高了预算利用率。

**6. token 估算**（无 tokenizer 依赖）

```
tokens = CJK字符数 × 1.0 + 其他字符数 × 0.28
```

0.28 是英文文本在 BPE 分词下的经验均值（约 3.6 字符/token）。CJK 按 1 字 1 token 偏保守，实际多数模型下略低，保守估算能保证不超预算。

---

## L4 · A2A Agent Card —— 能力自动发现

**端点**：`/.well-known/agent-card.json`（A2A v0.3 规定路径，早期版本用 `agent.json`，本站同时保留兼容别名）。

它回答的问题不是「这个站有什么内容」，而是「**这个站能被当成一个 Agent 调用吗、有哪些技能**」。

```jsonc
{
  "protocolVersion": "0.3.0",
  "name": "Evergreen Context Engine",
  "description": "...",
  "url": "https://evergreen.example.com/mcp",
  "preferredTransport": "JSONRPC",
  "capabilities": { "streaming": false, "pushNotifications": false, "extensions": [] },
  "defaultInputModes": ["text/plain", "application/json"],
  "defaultOutputModes": ["application/json", "text/markdown"],
  "securitySchemes": {},
  "security": [],
  "skills": [
    { "id": "search_context", "name": "...", "description": "...", "tags": ["retrieval"], "examples": ["..."] }
  ]
}
```

**为什么值得做**：支持 A2A 的编排框架（以及 Chrome 的 agentic browsing）会**主动**去 `.well-known/` 探测。探测到就自动挂载技能，用户完全不需要在提示词里解释这个站能干什么。这是七层里唯一能做到「零提示词接入」的一层。

`securitySchemes` 为空对象表示**免鉴权**。本站确实免鉴权，这一点必须在 `ai.txt` 与 Agent Card 里一致声明，否则 Agent 会浪费一轮去试探 401。

---

## L5 · MCP —— 让站点变成工具

**端点**：`POST /mcp`，JSON-RPC 2.0 over HTTP。

MCP（Model Context Protocol）与 L1~L4 的区别：前面几层是 Agent「读」站点，MCP 是 Agent「调用」站点。当知识量大到不适合一次装入上下文时，MCP 让 Agent 按需下钻。

**本站暴露 4 个工具** [EG-102]：

| 工具 | 入参 | 出参 |
|---|---|---|
| `search_context` | `query: string, limit?: number=5, budget?: number=2000` | 匹配条目数组，按相关度降序 |
| `get_entry` | `id: string` | 单条完整条目；ID 不存在时 `isError: true` 并列出最相近的 3 个 ID |
| `get_prompt_pack` | `scenario: "chat"\|"coding"\|"research"\|"support"` | 可直接注入系统提示的字符串 |
| `get_metrics` | 无 | 评测指标 JSON |

**握手序列**：

```http
POST /mcp
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"probe","version":"1.0"}}}

POST /mcp
{"jsonrpc":"2.0","method":"notifications/initialized"}      ← 通知，无 id，服务端回 202 空体

POST /mcp
{"jsonrpc":"2.0","id":2,"method":"tools/list"}

POST /mcp
{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"get_entry","arguments":{"id":"EG-201"}}}
```

**tools/call 的响应形状**（容易写错的地方）：

```jsonc
{
  "jsonrpc": "2.0",
  "id": 3,
  "result": {
    "content": [ { "type": "text", "text": "..." } ],   // 必须是数组
    "structuredContent": { "...": "..." },              // 可选，机器可读
    "isError": false                                     // 工具级错误用这个，不用 JSON-RPC error
  }
}
```

**关键区分**：
- **协议级错误**（方法不存在、参数缺失、JSON 解析失败）→ JSON-RPC 顶层 `error` 对象。
- **工具级错误**（ID 不存在、查询无结果）→ `result.isError = true` + 人类可读的 `content`。

搞混这两者的服务端，会让 Agent 把一个「查无此条」当成「连接故障」去无限重试。

**批量请求**：本站支持 JSON-RPC 的数组形式批量请求，返回数组形式的批量响应。

**推荐调用顺序**：`get_prompt_pack`（立规则）→ `search_context`（找事实）→ `get_entry`（深挖单条）[EG-401]。

---

## L6 · JSON-LD —— 实体消歧

内嵌在 HTML `<head>` 的 `application/ld+json`，用 Schema.org `@graph` 一次声明多个实体：

```jsonc
{
  "@context": "https://schema.org",
  "@graph": [
    { "@type": "WebSite", "name": "Evergreen", "url": "...", "inLanguage": ["zh-CN","en"] },
    { "@type": "Organization", "name": "...", "url": "..." },
    { "@type": "Dataset", "name": "Evergreen Knowledge Base", "distribution": [
        { "@type": "DataDownload", "encodingFormat": "application/json", "contentUrl": "/context.json" },
        { "@type": "DataDownload", "encodingFormat": "text/markdown", "contentUrl": "/llms-full.txt" }
    ], "license": "https://creativecommons.org/licenses/by/4.0/" },
    { "@type": "SoftwareApplication", "applicationCategory": "DeveloperApplication", "offers": { "@type": "Offer", "price": "0" } },
    { "@type": "FAQPage", "mainEntity": [ { "@type": "Question", "name": "...", "acceptedAnswer": { "@type": "Answer", "text": "..." } } ] }
  ]
}
```

**为什么有用**：
1. `Dataset` + `distribution` 让搜索引擎与 Agent 知道 `/context.json` 是**数据分发点**，而不只是一个随机 JSON 文件。
2. `FAQPage` 的问答对会被直接抽取为答案，绕过页面正文。
3. `@graph` 让多个实体共享同一个上下文，避免重复声明 `@context`。

L6 是七层里**唯一无需 Agent 做任何适配就能生效**的一层——所有主流爬虫早已支持 JSON-LD。

---

## L7 · robots.txt / ai.txt / ai-policy.json —— 准入与授权

### robots.txt

显式放行 AI 爬虫，而不是一句 `User-agent: * / Allow: /` 了事：

```
User-agent: GPTBot
Allow: /

User-agent: OAI-SearchBot
Allow: /

User-agent: ChatGPT-User
Allow: /

User-agent: ClaudeBot
Allow: /

User-agent: Claude-Web
Allow: /

User-agent: PerplexityBot
Allow: /

User-agent: Google-Extended
Allow: /

User-agent: CCBot
Allow: /

User-agent: Applebot-Extended
Allow: /

User-agent: Bytespider
Allow: /
```

**显式声明的价值**：`User-agent: *` 在语义上覆盖它们，但部分爬虫实现只认自己的专属块。显式列出还能传达「本站主动欢迎 AI」的意图，这在 `ai.txt` 里进一步形式化。

必须给出 `Sitemap:` 行。

### ai.txt

ai.txt 是 2025 年提出的提案，**纯文本**格式（不是 JSON），三个核心段：

```
# AI usage policy for evergreen.example.com
# Format: ai.txt (plain text directives)

Permissions: allow-crawl, allow-index, allow-inference, allow-rag, allow-summarize, allow-quote, allow-citation
Restrictions: no-paywall-bypass, no-impersonation, no-misattribution, respect-rate-limit
Content policies: attribution-required, no-factual-alteration, license=CC-BY-4.0
Rate limit: 120 requests/minute, burst 300
Machine-readable endpoints: /llms.txt, /llms-full.txt, /context.json, /api/context, /mcp, /.well-known/agent-card.json
Contact: ai@evergreen.example.com
```

### ai-policy.json

同一策略的机器可读版本，供程序化校验。**两者必须同源**，否则声明冲突时 Agent 会取更严格的那个。

---

## 层间关系：Agent 实际怎么走

```
用户粘贴链接给 AI
      │
      ▼
GET /                       ← HTML，200
      │  Link: </index.html.md>; rel="alternate"; type="text/markdown"
      │  Link: </llms.txt>; rel="describedby"
      ├──────────────┐
      ▼              ▼
GET /llms.txt    GET /index.html.md
  (900 tok)         (4050 tok)
      │
      │  读到「首选入口是 /api/context」
      ▼
GET /api/context?topic=<用户问题>&budget=2000
      │
      │  truncated == true ?
      ├── false ──► 直接作答，每个断言附 [EG-xxx]
      │
      └── true ───► 提高 budget 重试
                    或 POST /mcp tools/call get_entry 下钻单条
```

**总请求数：2~3 次。** 对比无 Evergreen 的 4.2 次 [EG-003]。

---

## 优先级建议

如果只能做一部分，按这个顺序：

| 阶段 | 做什么 | 工作量 | 收益占比 |
|---|---|---|---|
| **P0** | L1 llms.txt + L2 首页 .md 镜像 + L7 robots.txt 放行 | 半天 | ~70% |
| **P1** | L3 context.json + L6 JSON-LD | 1~2 天 | ~20% |
| **P2** | L3 /api/context 动态查询 + L5 MCP + L4 Agent Card | 3~5 天 | ~10% |

P0 之所以占七成，是因为它解决了「Agent 找不到入口、读到的全是噪声」这个最贵的问题。P2 的边际收益递减，但在知识库超过 200 条时变成必需 [EG-402]。

详见 [/docs/migration.md](/docs/migration.md)。
