Phase 0 · Agent 核心循环

v4: 技能机制

原创教程·2026/7/22·8 阅读

v4: 技能机制

核心洞察:技能是知识包,不是工具。

知识外化:从训练到编辑

技能体现了一种深刻的范式转变:知识外化

传统方法:内化在参数中的知识

传统的 AI 系统将所有知识存储在模型参数中。你无法访问、修改或重用它。

想要模型学习新技能?你需要:

  1. 收集大量训练数据
  2. 设置分布式训练集群
  3. 执行复杂的参数微调(LoRA、全量微调等)
  4. 部署新的模型版本

这就像你的大脑突然失去记忆,但你没有笔记可以恢复。知识被锁定在神经网络的权重矩阵中,对用户完全不透明。

新范式:外化为文档的知识

代码执行范式改变了一切。

┌──────────────────────────────────────────────────────────────────────┐
│                        知识存储层级                                    │
│                                                                      │
│  模型参数 → 上下文窗口 → 文件系统 → 技能库                            │
│   (内化的)     (运行时)    (持久的)    (结构化的)                      │
│                                                                      │
│  ←────── 需要训练 ──────→  ←─── 自然语言编辑 ────→                  │
│    需要集群、数据、专业知识        任何人都可以修改                     │
└──────────────────────────────────────────────────────────────────────┘

关键突破

  • 之前:修改模型行为 = 修改参数 = 需要训练 = GPU 集群 + 训练数据 + ML 专业知识
  • 现在:修改模型行为 = 编辑 SKILL.md = 编辑文本文件 = 任何人都可以做到

这就像给基础模型附加一个可热插拔的 LoRA 适配器,但不需要任何参数训练。

为什么这很重要

  1. 民主化:无需 ML 专业知识即可自定义模型行为
  2. 透明度:知识以人类可读的 Markdown 存储,可审计和理解
  3. 可重用性:编写一次技能,可在任何兼容的代理上使用
  4. 版本控制:Git 管理知识变更,支持协作和回滚
  5. 在线学习:模型在更大的上下文窗口中"学习",无需离线训练

传统微调是离线学习:收集数据 → 训练 → 部署 → 使用。 技能实现了在线学习:在运行时按需加载知识,立即生效。

知识层级对比

层级修改方式生效时间持久性成本
模型参数训练/微调几小时到几天永久$10K-$1M+
上下文窗口API 调用立即每次会话~$0.01/调用
文件系统编辑文件下次加载永久免费
技能库编辑 SKILL.md下次触发永久免费

技能达到了最佳平衡点:持久存储 + 按需加载 + 人类可编辑。

实际示例

假设你希望 Claude 学习你公司特定的编码标准:

传统方式

1. 收集公司代码库作为训练数据
2. 准备微脚本和基础设施
3. 运行 LoRA 微调(需要 GPU)
4. 部署自定义模型
5. 成本:$1000+,时间:数周

技能方式

# skills/company-standards/SKILL.md
---
name: company-standards
description: 公司编码标准和最佳实践
---

## 命名规范
- 函数使用小写加下划线
- 类使用 PascalCase
...
成本:$0,时间:5 分钟

这就是知识外化的力量:将曾经需要训练才能编码的知识变成任何人都可以编辑的文档

问题所在

v3 为我们提供了用于任务分解的子代理。但还有一个更深层次的问题:模型如何知道如何处理特定领域的任务?

  • 处理 PDF?它需要知道 pdftotext vs PyMuPDF
  • 构建 MCP 服务器?它需要协议规范和最佳实践
  • 代码审查?它需要系统的检查清单

这些知识不是工具——它是专业知识。技能通过让模型按需加载领域知识来解决这个问题。

核心概念

1. 工具 vs 技能

概念是什么示例
工具模型能做什么bash, read_file, write_file
技能模型知道如何做PDF 处理, MCP 构建

工具是能力。技能是知识。

2. 渐进式披露

第 1 层:元数据(始终加载)    ~100 令牌/技能
         └─ 名称 + 描述

第 2 层:SKILL.md 正文(触发时) ~2000 令牌
         └─ 详细说明

第 3 层:资源(按需)        无限制
         └─ scripts/, references/, assets/

这保持上下文精简,同时允许任意深度的知识。

3. SKILL.md 标准

skills/
├── pdf/
│   └── SKILL.md          # 必需
├── mcp-builder/
│   ├── SKILL.md
│   └── references/       # 可选
└── code-review/
    ├── SKILL.md
    └── scripts/          # 可选

SKILL.md 格式:YAML 前言 + Markdown 正文

---
name: pdf
description: 处理 PDF 文件。在读取、创建或合并 PDF 时使用。
---

# PDF 处理技能

## 读取 PDF

使用 pdftotext 快速提取:
```bash
pdftotext input.pdf -

...


## 实现(新增约 100 行)

### SkillLoader 类

```python
class SkillLoader:
    def __init__(self, skills_dir: Path):
        self.skills = {}
        self.load_skills()

    def parse_skill_md(self, path: Path) -> dict:
        """解析 YAML 前言 + Markdown 正文。"""
        content = path.read_text()
        match = re.match(r"^---\s*\n(.*?)\n---\s*\n(.*)$", content, re.DOTALL)
        # 返回 {name, description, body, path, dir}

    def get_descriptions(self) -> str:
        """生成系统提示的元数据。"""
        return "\n".join(f"- {name}: {skill['description']}"
                        for name, skill in self.skills.items())

    def get_skill_content(self, name: str) -> str:
        """获取完整内容用于上下文注入。"""
        return f"# Skill: {name}\n\n{skill['body']}"

Skill 工具

SKILL_TOOL = {
    "name": "Skill",
    "description": "加载技能以获得任务的专业知识。",
    "input_schema": {
        "properties": {"skill": {"type": "string"}},
        "required": ["skill"]
    }
}

消息注入(保留缓存)

关键洞察:技能内容进入 tool_result(用户消息的一部分),而不是系统提示:

def run_skill(skill_name: str) -> str:
    content = SKILLS.get_skill_content(skill_name)
    # 完整内容作为 tool_result 返回
    # 成为对话历史(用户消息)的一部分
    return f"""<skill-loaded name="{skill_name}">
{content}
</skill-loaded>

遵循上面技能中的说明完成用户的任务。"""

def agent_loop(messages: list) -> list:
    while True:
        response = client.messages.create(
            model=MODEL,
            system=SYSTEM,  # 从不更改 - 缓存被保留!
            messages=messages,
            tools=ALL_TOOLS,
        )
        # 技能内容作为 tool_result 进入消息...

关键洞察

  • 技能内容追加到末尾作为新消息
  • 之前的所有内容(系统提示 + 所有先前消息)都被缓存并重用
  • 只有新追加的技能内容需要计算 — 整个前缀命中缓存

与生产系统的对比

机制Claude Code / Kodev4
格式SKILL.md(YAML + MD)相同
加载Container APISkillLoader 类
触发自动 + Skill 工具仅 Skill 工具
注入newMessages(用户消息)tool_result(用户消息)
缓存追加到末尾,整个前缀被缓存追加到末尾,整个前缀被缓存
版本控制Skill Versions API省略
权限allowed-tools 字段省略

关键相似之处:两者都将技能内容注入对话历史(而不是系统提示),保留提示缓存。

为什么这很重要:缓存经济学

忽略缓存的代价

许多使用 LangGraph、LangChain、AutoGen 的开发者习惯性地:

  • 将动态状态注入系统提示
  • 编辑和压缩消息历史
  • 使用滑动窗口截断对话

这些操作会使缓存失效,使成本暴涨 7-50 倍。

一个典型的 50 轮 SWE 任务:

  • 缓存破坏:$14.06(每轮修改系统提示)
  • 缓存优化:$1.85(仅追加方式)
  • 节省:86.9%

对于每天处理 100 个任务的应用程序,这意味着 $45,000+ 的年度节省

自回归模型和 KV 缓存

LLM 是自回归的:生成每个令牌都需要关注所有先前的令牌。为了避免冗余计算,提供商实现了 KV 缓存

请求 1:[System, User1, Asst1, User2]
           ←────── 全部计算 ─────→

请求 2:[System, User1, Asst1, User2, Asst2, User3]
           ←────── 缓存命中 ─────→ ←─ 新内容 ─→
                   (0.1x 价格)        (正常价格)

缓存命中需要精确的前缀匹配。修改系统提示或历史记录会使整个前缀缓存失效。

常见反模式

反模式影响成本乘数
动态系统提示100% 缓存未命中20-50 倍
消息压缩从替换点开始失效5-15 倍
滑动窗口100% 缓存未命中30-50 倍
消息编辑从编辑点开始失效10-30 倍
多代理全网状上下文爆炸3-4 倍(vs 单代理)

提供商差异

提供商自动缓存折扣配置
Claude90%需要 cache_control
GPT-5.290%无需配置
Kimi K290%无需配置
GLM-4.782%无需配置
MiniMax M2.190%需要 cache_control
Gemini 3✓(隐式)90%无需配置

重要:Claude 和 MiniMax 需要显式的 cache_control 配置 — 否则不会有缓存命中。

推荐:仅追加方式

# 错误:编辑历史
messages[2]["content"] = "已编辑"  # 缓存失效!

# 正确:仅追加
messages.append(new_msg)  # 前缀不变,缓存命中

# 错误:动态系统提示
system = f"状态:{state}"  # 每次都变!

# 正确:固定系统,状态在消息中
SYSTEM = "你是一个助手。"  # 从不更改
messages.append({"role": "user", "content": f"状态:{state}"})

上下文长度支持

现代模型支持大上下文窗口:

  • Claude Sonnet 4.5 / Opus 4.5:200K
  • GPT-5.2:256K+
  • Gemini 3 Flash/Pro:1M-2M

200K 令牌 ≈ 15 万字 ≈ 一本 500 页的书。对于大多数 Agent 任务,现有上下文窗口已足够。

将上下文视为仅追加日志,而非可编辑文档。

深入探讨

关于缓存经济学的全面覆盖:

  1. 常见反模式:LangGraph/LangChain 中的 5 个破坏缓存的错误
  2. 详细计算:50 轮 SWE 任务的逐轮成本分析
  3. 提供商策略:各提供商的缓存机制和定价对比
  4. 代理编排:令牌消耗差异(多代理 ~3-4 倍 vs 单代理)
  5. 最佳实践:如何检测和修复破坏缓存的问题

参见:上下文缓存经济学:Agent 开发者的成本优化指南(中文)

哲学:实践中的知识外化

知识作为一等公民

回到开始时讨论的知识外化范式。传统观点:AI 代理是"工具调用者"——模型决定使用哪个工具,代码执行它。

但这漏掉了一个关键维度:模型如何知道该做什么?

技能是知识外化的完整实践:

之前(知识内化)

  • 知识锁定在模型参数中
  • 修改需要训练(LoRA、全量微调)
  • 用户无法访问或理解
  • 成本:$10K-$1M+,时间线:数周

现在(知识外化)

  • 知识存储在 SKILL.md 文件中
  • 修改只是编辑文本
  • 人类可读、可审计
  • 成本:免费,时间线:即时

技能承认领域知识本身就是一种需要明确管理的资源

  1. 分离元数据和内容:描述是索引,正文是内容
  2. 按需加载:上下文窗口是宝贵的认知资源
  3. 标准化格式:编写一次,在任何兼容代理中使用
  4. 注入,不返回:技能改变认知,而不只是提供数据
  5. 在线学习:在更大的上下文窗口中即时学习,无需离线训练

知识外化的本质是将隐性知识变成显性文档

  • 开发者用自然语言"教"模型新技能
  • Git 管理和共享知识
  • 版本控制、审计、回滚

这是从"训练 AI"到"教育 AI"的范式转变。

系列总结

版本主题新增代码行关键洞察
v1模型即代理~200模型占 80%,代码只是循环
v2结构化规划~100Todo 使计划可见
v3分而治之~150子代理隔离上下文
v4领域专家~100技能注入专业知识

学习笔记

按此顺序阅读源代码

打开 v4_skills_agent.py,先阅读以下部分:

  1. SKILLS_DIR
  2. SkillLoader
  3. SKILLS = SkillLoader(SKILLS_DIR)
  4. SYSTEM
  5. SKILL_TOOL
  6. run_skill
  7. execute_tool
  8. agent_loop

v4 是 v3 加上一个新想法:

工具让模型行动
技能让模型知道如何在领域中行动

技能目录是知识库

代码指向:

SKILLS_DIR = WORKDIR / "skills"

每个技能都是一个包含必需的 SKILL.md 的文件夹:

skills/
  pdf/
    SKILL.md
  mcp-builder/
    SKILL.md
  code-review/
    SKILL.md

这是从隐藏知识到可编辑知识的核心转变。团队可以通过编辑 Git 中的 Markdown 文件来更改代理的行为。

SkillLoader 分离元数据和完整内容

SkillLoader 不会将每个技能都转储到提示中。它首先索引廉价的元数据:

def get_descriptions(self) -> str:
    return "\n".join(
        f"- {name}: {skill['description']}"
        for name, skill in self.skills.items()
    )

该元数据被添加到系统提示中:

**可用技能**(当任务匹配时使用 Skill 工具调用):
{SKILLS.get_descriptions()}

模型可以看到存在哪些技能,但昂贵的正文在需要之前不会进入上下文。

加载技能是一次工具调用

Skill 工具的模式是:

SKILL_TOOL = {
    "name": "Skill",
    "description": "加载技能以获得任务的专业知识。",
    ...
}

当模型调用它时,调度程序将其路由到:

if name == "Skill":
    return run_skill(args["skill"])

这意味着技能适合与其他每个行动相同的循环:

模型选择 Skill
主机加载 SKILL.md
主机返回技能内容作为 tool_result
模型在新知识下继续

runskill 是关键机制

v4 的核心是:

def run_skill(skill_name: str) -> str:
    content = SKILLS.get_skill_content(skill_name)
    return f"""<skill-loaded name="{skill_name}">
{content}
</skill-loaded>

遵循上面技能中的说明完成用户的任务。"""

这与从搜索工具返回数据不同。技能改变了模型处理其余任务的方式。

可以把它想象成暂时在对话中添加一个专门的操作手册。

为什么技能内容进入 toolresult

源代码明确说明了这种设计:

为什么是 tool_result 而不是系统提示?
- 系统提示更改会使缓存失效
- 工具结果追加到末尾

这是一个微妙但重要的生产教训。保持系统提示稳定。将新信息追加到消息中。

这保留了提示缓存:

稳定前缀 -> 缓存命中
新技能内容 -> 只处理新后缀

技能加载有三层

代码实现了渐进式披露:

层级源代码位置模型看到什么
元数据get_descriptions()名称 + 描述
正文get_skill_content()完整的 SKILL.md 正文
资源scripts/, references/, assets/ 提示要检查或运行的额外文件

这让代理知道存在哪些技能,而无需预先支付加载每个手册的上下文成本。

v4 如何结合早期版本

v4 仍然包含早期机制:

v1: bash/read/write/edit 工具
v2: TodoWrite
v3: Task 子代理
v4: Skill 加载

ALL_TOOLS 中,最终代理获得:

ALL_TOOLS = BASE_TOOLS + [TASK_TOOL, SKILL_TOOL]

所以 v4 不是不同的架构。它是相同的循环,只是有更精心设计的上下文源。

工具 vs 技能:实际测试

问这个问题:

这是帮助模型做某事,还是知道如何做某事?

如果它执行一个动作,它可能是一个工具:

bash
read_file
write_file
edit_file
Task

如果它教授一种方法、检查清单、惯例或领域工作流程,它可能是一个技能:

code-review
mcp-builder
pdf
agent-builder

常见失败模式

在构建你自己的技能时,避免:

  1. 描述太模糊。模型将不知道何时加载技能。
  2. 巨大的始终加载的提示。这违背了渐进式披露的目的。
  3. 把秘密放在技能中。技能是文件;将它们视为仓库内容。
  4. 混合工具和技能。技能可以提到脚本,但执行仍然通过工具发生。
  5. 每次都更改系统提示。更喜欢仅追加的消息注入以实现缓存友好性。

学习检查

阅读代码后,确保你能回答:

  • v4 在哪里发现可用的技能?
  • 哪个方法只读取技能元数据?
  • 哪个方法加载完整的技能正文?
  • 为什么 run_skill 返回 XML 风格的标签?
  • 为什么技能内容作为 tool_result 返回而不是插入到系统提示中?
  • 技能如何与待办事项和子代理结合?

完整源代码

#!/usr/bin/env python3
"""
v4_skills_agent.py - Mini Claude Code:技能机制(约 550 行)

核心理念:"知识外化"
========================================
v3 为我们提供了用于任务分解的子代理。但还有一个更深层次的问题:

    模型如何知道如何处理特定领域的任务?

- 处理 PDF?它需要知道 pdftotext vs PyMuPDF
- 构建 MCP 服务器?它需要协议规范和最佳实践
- 代码审查?它需要系统的检查清单

这些知识不是工具 - 它是专业知识。技能通过让
模型按需加载领域知识来解决这个问题。

范式转变:知识外化
------------------------------------
传统 AI:知识锁定在模型参数中
  - 教授新技能:收集数据 -> 训练 -> 部署
  - 成本:$10K-$1M+,时间线:数周
  - 需要 ML 专业知识、GPU 集群

技能:知识存储在可编辑的文件中
  - 教授新技能:编写 SKILL.md 文件
  - 成本:免费,时间线:分钟
  - 任何人都可以做到

这就像附加一个可热插拔的 LoRA 适配器,无需任何训练!

工具 vs 技能:
---------------
    | 概念     | 是什么                  | 示例                        |
    |-----------|-------------------------|---------------------------- |
    | **工具**  | 模型能做什么             | bash, read_file, write     |
    | **技能**  | 模型知道如何做           | PDF 处理, MCP 开发          |

工具是能力。技能是知识。

渐进式披露:
------------------
    第 1 层:元数据(始终加载)      ~100 令牌/技能
             仅名称 + 描述

    第 2 层:SKILL.md 正文(触发时)   ~2000 令牌
             详细说明

    第 3 层:资源(按需)         无限制
             scripts/, references/, assets/

这保持上下文精简,同时允许任意深度。

SKILL.md 标准:
-----------------
    skills/
    |-- pdf/
    |   |-- SKILL.md          # 必需:YAML 前言 + Markdown 正文
    |-- mcp-builder/
    |   |-- SKILL.md
    |   |-- references/       # 可选:文档、规范
    |-- code-review/
        |-- SKILL.md
        |-- scripts/          # 可选:辅助脚本

保留缓存的注入:
--------------------------
关键洞察:技能内容进入 tool_result(用户消息),
而不是系统提示。这保留了提示缓存!

    错误:每次编辑系统提示(缓存失效,20-50 倍成本增加)
    正确:将技能追加为工具结果(前缀不变,缓存命中)

这就是生产 Claude Code 的工作方式 - 这也是它具有成本效益的原因。

使用方法:
    python v4_skills_agent.py
"""

import os
import re
import subprocess
import sys
import time
from pathlib import Path

from dotenv import load_dotenv

load_dotenv()

try:
    from anthropic import Anthropic
except ImportError:
    sys.exit("请安装:pip install anthropic python-dotenv")


# =============================================================================
# 配置
# =============================================================================

API_KEY = os.getenv("ANTHROPIC_API_KEY")
BASE_URL = os.getenv("ANTHROPIC_BASE_URL")
MODEL = os.getenv("MODEL_NAME", "claude-sonnet-4-20250514")
WORKDIR = Path.cwd()
SKILLS_DIR = WORKDIR / "skills"

client = Anthropic(api_key=API_KEY, base_url=BASE_URL) if BASE_URL else Anthropic(api_key=API_KEY)


# =============================================================================
# SkillLoader - v4 中的核心新增
# =============================================================================

class SkillLoader:
    """
    从 SKILL.md 文件加载和管理技能。

    技能是一个包含以下内容的文件夹:
    - SKILL.md(必需):YAML 前言 + markdown 说明
    - scripts/(可选):模型可以运行的辅助脚本
    - references/(可选):额外文档
    - assets/(可选):模板、输出文件

    SKILL.md 格式:
    ----------------
        ---
        name: pdf
        description: 处理 PDF 文件。在读取、创建或合并 PDF 时使用。
        ---

        # PDF 处理技能

        ## 读取 PDF

        使用 pdftotext 快速提取:
        ```bash
        pdftotext input.pdf -
        ```
        ...

    YAML 前言提供元数据(名称、描述)。
    Markdown 正文提供详细说明。
    """

    def __init__(self, skills_dir: Path):
        self.skills_dir = skills_dir
        self.skills = {}
        self.load_skills()

    def parse_skill_md(self, path: Path) -> dict:
        """
        解析 SKILL.md 文件为元数据和正文。

        返回包含以下内容的 dict:name, description, body, path, dir
        如果文件格式不匹配则返回 None。
        """
        content = path.read_text()

        # 匹配 --- 标记之间的 YAML 前言
        match = re.match(r"^---\s*\n(.*?)\n---\s*\n(.*)$", content, re.DOTALL)
        if not match:
            return None

        frontmatter, body = match.groups()

        # 解析类似 YAML 的前言(简单键:值)
        metadata = {}
        for line in frontmatter.strip().split("\n"):
            if ":" in line:
                key, value = line.split(":", 1)
                metadata[key.strip()] = value.strip().strip("\"'")

        # 要求名称和描述
        if "name" not in metadata or "description" not in metadata:
            return None

        return {
            "name": metadata["name"],
            "description": metadata["description"],
            "body": body.strip(),
            "path": path,
            "dir": path.parent,
        }

    def load_skills(self):
        """
        扫描技能目录并加载所有有效的 SKILL.md 文件。

        启动时仅加载元数据 - 正文按需加载。
        这保持初始上下文精简。
        """
        if not self.skills_dir.exists():
            return

        for skill_dir in self.skills_dir.iterdir():
            if not skill_dir.is_dir():
                continue

            skill_md = skill_dir / "SKILL.md"
            if not skill_md.exists():
                continue

            skill = self.parse_skill_md(skill_md)
            if skill:
                self.skills[skill["name"]] = skill

    def get_descriptions(self) -> str:
        """
        生成系统提示的技能描述。

        这是第 1 层 - 仅名称和描述,每个技能 ~100 令牌。
        完整内容(第 2 层)仅在调用 Skill 工具时加载。
        """
        if not self.skills:
            return "(无可用技能)"

        return "\n".join(
            f"- {name}: {skill['description']}"
            for name, skill in self.skills.items()
        )

    def get_skill_content(self, name: str) -> str:
        """
        获取完整技能内容用于注入。

        这是第 2 层 - 完整的 SKILL.md 正文,加上任何可用的
        资源(第 3 层提示)。

        如果未找到技能则返回 None。
        """
        if name not in self.skills:
            return None

        skill = self.skills[name]
        content = f"# Skill: {skill['name']}\n\n{skill['body']}"

        # 列出可用资源(第 3 层提示)
        resources = []
        for folder, label in [
            ("scripts", "脚本"),
            ("references", "参考文献"),
            ("assets", "资产")
        ]:
            folder_path = skill["dir"] / folder
            if folder_path.exists():
                files = list(folder_path.glob("*"))
                if files:
                    resources.append(f"{label}{', '.join(f.name for f in files)}")

        if resources:
            content += f"\n\n**{skill['dir']} 中的可用资源:**\n"
            content += "\n".join(f"- {r}" for r in resources)

        return content

    def list_skills(self) -> list:
        """返回可用技能名称列表。"""
        return list(self.skills.keys())


# 全局技能加载器实例
SKILLS = SkillLoader(SKILLS_DIR)


# =============================================================================
# 代理类型注册表(来自 v3)
# =============================================================================

AGENT_TYPES = {
    "explore": {
        "description": "用于探索代码、查找文件、搜索的只读代理",
        "tools": ["bash", "read_file"],
        "prompt": "你是一个探索代理。搜索和分析,但永远不要修改文件。返回简洁的摘要。",
    },
    "code": {
        "description": "用于实现功能和修复错误的完整代理",
        "tools": "*",
        "prompt": "你是一个编码代理。高效地实现所请求的更改。",
    },
    "plan": {
        "description": "用于设计实施策略的规划代理",
        "tools": ["bash", "read_file"],
        "prompt": "你是一个规划代理。分析代码库并输出编号的实施计划。不要做任何更改。",
    },
}


def get_agent_descriptions() -> str:
    """为系统提示生成代理类型描述。"""
    return "\n".join(
        f"- {name}: {cfg['description']}"
        for name, cfg in AGENT_TYPES.items()
    )


# =============================================================================
# TodoManager(来自 v2)
# =============================================================================

class TodoManager:
    """带约束的任务列表管理器。详见 v2。"""

    def __init__(self):
        self.items = []

    def update(self, items: list) -> str:
        validated = []
        in_progress = 0

        for i, item in enumerate(items):
            content = str(item.get("content", "")).strip()
            status = str(item.get("status", "pending")).lower()
            active = str(item.get("activeForm", "")).strip()

            if not content or not active:
                raise ValueError(f"项目 {i}:需要 content 和 activeForm")
            if status not in ("pending", "in_progress", "completed"):
                raise ValueError(f"项目 {i}:无效状态")
            if status == "in_progress":
                in_progress += 1

            validated.append({
                "content": content,
                "status": status,
                "activeForm": active
            })

        if in_progress > 1:
            raise ValueError("只能有一个任务处于 in_progress 状态")

        self.items = validated[:20]
        return self.render()

    def render(self) -> str:
        if not self.items:
            return "无待办事项。"
        lines = []
        for t in self.items:
            mark = "[x]" if t["status"] == "completed" else \
                   "[>]" if t["status"] == "in_progress" else "[ ]"
            lines.append(f"{mark} {t['content']}")
        done = sum(1 for t in self.items if t["status"] == "completed")
        return "\n".join(lines) + f"\n({done}/{len(self.items)} 已完成)"


TODO = TodoManager()


# =============================================================================
# 系统提示 - v4 更新
# =============================================================================

SYSTEM = f"""你是位于 {WORKDIR} 的编码代理。

循环:规划 -> 使用工具执行 -> 报告。

**可用技能**(当任务匹配时立即使用 Skill 工具):
{SKILLS.get_descriptions()}

**可用代理**(对于聚焦的子任务调用 Task 工具):
{get_agent_descriptions()}

规则:
- 当用户任务匹配技能描述时立即使用 Skill 工具
- 对于需要聚焦探索或实施的子任务使用 Task 工具
- 使用 TodoWrite 跟踪多步骤工作
- 优先使用工具而非文字说明。行动,不要只是解释。
- 完成后,总结所做的更改。"""


# =============================================================================
# 工具定义
# =============================================================================

BASE_TOOLS = [
    {
        "name": "bash",
        "description": "运行 shell 命令。",
        "input_schema": {
            "type": "object",
            "properties": {"command": {"type": "string"}},
            "required": ["command"],
        },
    },
    {
        "name": "read_file",
        "description": "读取文件内容。",
        "input_schema": {
            "type": "object",
            "properties": {
                "path": {"type": "string"},
                "limit": {"type": "integer"}
            },
            "required": ["path"],
        },
    },
    {
        "name": "write_file",
        "description": "写入文件。",
        "input_schema": {
            "type": "object",
            "properties": {
                "path": {"type": "string"},
                "content": {"type": "string"}
            },
            "required": ["path", "content"],
        },
    },
    {
        "name": "edit_file",
        "description": "替换文件中的文本。",
        "input_schema": {
            "type": "object",
            "properties": {
                "path": {"type": "string"},
                "old_text": {"type": "string"},
                "new_text": {"type": "string"},
            },
            "required": ["path", "old_text", "new_text"],
        },
    },
    {
        "name": "TodoWrite",
        "description": "更新任务列表。",
        "input_schema": {
            "type": "object",
            "properties": {
                "items": {
                    "type": "array",
                    "items": {
                        "type": "object",
                        "properties": {
                            "content": {"type": "string"},
                            "status": {
                                "type": "string",
                                "enum": ["pending", "in_progress", "completed"]
                            },
                            "activeForm": {"type": "string"},
                        },
                        "required": ["content", "status", "activeForm"],
                    },
                }
            },
            "required": ["items"],
        },
    },
]

# Task 工具(来自 v3)
TASK_TOOL = {
    "name": "Task",
    "description": f"""生成子代理处理聚焦的子任务。

代理类型:
{get_agent_descriptions()}""",
    "input_schema": {
        "type": "object",
        "properties": {
            "description": {
                "type": "string",
                "description": "简短任务描述(3-5 个词)"
            },
            "prompt": {
                "type": "string",
                "description": "给子代理的详细指令"
            },
            "agent_type": {
                "type": "string",
                "enum": list(AGENT_TYPES.keys())
            },
        },
        "required": ["description", "prompt", "agent_type"],
    },
}

# v4 新增:Skill 工具
SKILL_TOOL = {
    "name": "Skill",
    "description": f"""加载技能以获得任务的专业知识。

可用技能:
{SKILLS.get_descriptions()}

使用时机:
- 当用户任务匹配技能描述时立即使用
- 在尝试特定领域工作之前(PDF、MCP 等)

技能内容将被注入到对话中,为你提供
详细说明和资源访问权限。""",
    "input_schema": {
        "type": "object",
        "properties": {
            "skill": {
                "type": "string",
                "description": "要加载的技能名称"
            }
        },
        "required": ["skill"],
    },
}

ALL_TOOLS = BASE_TOOLS + [TASK_TOOL, SKILL_TOOL]


def get_tools_for_agent(agent_type: str) -> list:
    """根据代理类型过滤工具。"""
    allowed = AGENT_TYPES.get(agent_type, {}).get("tools", "*")
    if allowed == "*":
        return BASE_TOOLS
    return [t for t in BASE_TOOLS if t["name"] in allowed]


# =============================================================================
# 工具实现
# =============================================================================

def safe_path(p: str) -> Path:
    """确保路径保持在工作区内。"""
    path = (WORKDIR / p).resolve()
    if not path.is_relative_to(WORKDIR):
        raise ValueError(f"路径超出工作区:{p}")
    return path


def run_bash(cmd: str) -> str:
    """执行 shell 命令。"""
    if any(d in cmd for d in ["rm -rf /", "sudo", "shutdown"]):
        return "错误:危险命令"
    try:
        r = subprocess.run(
            cmd, shell=True, cwd=WORKDIR,
            capture_output=True, text=True, timeout=60
        )
        return ((r.stdout + r.stderr).strip() or "(无输出)")[:50000]
    except Exception as e:
        return f"错误:{e}"


def run_read(path: str, limit: int = None) -> str:
    """读取文件内容。"""
    try:
        lines = safe_path(path).read_text().splitlines()
        if limit:
            lines = lines[:limit]
        return "\n".join(lines)[:50000]
    except Exception as e:
        return f"错误:{e}"


def run_write(path: str, content: str) -> str:
    """写入文件内容。"""
    try:
        fp = safe_path(path)
        fp.parent.mkdir(parents=True, exist_ok=True)
        fp.write_text(content)
        return f"已向 {path} 写入 {len(content)} 字节"
    except Exception as e:
        return f"错误:{e}"


def run_edit(path: str, old_text: str, new_text: str) -> str:
    """替换文件中的精确文本。"""
    try:
        fp = safe_path(path)
        text = fp.read_text()
        if old_text not in text:
            return f"错误:在 {path} 中未找到文本"
        fp.write_text(text.replace(old_text, new_text, 1))
        return f"已编辑 {path}"
    except Exception as e:
        return f"错误:{e}"


def run_todo(items: list) -> str:
    """更新待办事项列表。"""
    try:
        return TODO.update(items)
    except Exception as e:
        return f"错误:{e}"


def run_skill(skill_name: str) -> str:
    """
    加载技能并将其注入对话。

    这是关键机制:
    1. 获取技能内容(SKILL.md 正文 + 资源提示)
    2. 将其包装在 <skill-loaded> 标签中返回
    3. 模型将此作为 tool_result(用户消息)接收
    4. 模型现在"知道"如何执行任务

    为什么是 tool_result 而不是系统提示?
    - 系统提示更改会使缓存失效(成本增加 20-50 倍)
    - 工具结果追加到末尾(前缀不变,缓存命中)

    这就是生产系统保持成本效益的方式。
    """
    content = SKILLS.get_skill_content(skill_name)

    if content is None:
        available = ", ".join(SKILLS.list_skills()) or "无"
        return f"错误:未知技能 '{skill_name}'。可用:{available}"

    # 包装在标签中,以便模型知道这是技能内容
    return f"""<skill-loaded name="{skill_name}">
{content}
</skill-loaded>

遵循上面技能中的说明完成用户的任务。"""


def run_task(description: str, prompt: str, agent_type: str) -> str:
    """执行子代理任务(来自 v3)。详见 v3。"""
    if agent_type not in AGENT_TYPES:
        return f"错误:未知代理类型 '{agent_type}'"

    config = AGENT_TYPES[agent_type]
    sub_system = f"""你是位于 {WORKDIR}{agent_type} 子代理。

{config["prompt"]}

完成任务并返回清晰、简洁的摘要。"""

    sub_tools = get_tools_for_agent(agent_type)
    sub_messages = [{"role": "user", "content": prompt}]

    print(f"  [{agent_type}] {description}")
    start = time.time()
    tool_count = 0

    while True:
        response = client.messages.create(
            model=MODEL,
            system=sub_system,
            messages=sub_messages,
            tools=sub_tools,
            max_tokens=8000,
        )

        if response.stop_reason != "tool_use":
            break

        tool_calls = [b for b in response.content if b.type == "tool_use"]
        results = []

        for tc in tool_calls:
            tool_count += 1
            output = execute_tool(tc.name, tc.input)
            results.append({
                "type": "tool_result",
                "tool_use_id": tc.id,
                "content": output
            })

            elapsed = time.time() - start
            sys.stdout.write(
                f"\r  [{agent_type}] {description} ... {tool_count} 个工具, {elapsed:.1f}秒"
            )
            sys.stdout.flush()

        sub_messages.append({"role": "assistant", "content": response.content})
        sub_messages.append({"role": "user", "content": results})

    elapsed = time.time() - start
    sys.stdout.write(
        f"\r  [{agent_type}] {description} - 完成({tool_count} 个工具,{elapsed:.1f}秒)\n"
    )

    for block in response.content:
        if hasattr(block, "text"):
            return block.text

    return "(子代理未返回文本)"


def execute_tool(name: str, args: dict) -> str:
    """将工具调用分派到实现。"""
    if name == "bash":
        return run_bash(args["command"])
    if name == "read_file":
        return run_read(args["path"], args.get("limit"))
    if name == "write_file":
        return run_write(args["path"], args["content"])
    if name == "edit_file":
        return run_edit(args["path"], args["old_text"], args["new_text"])
    if name == "TodoWrite":
        return run_todo(args["items"])
    if name == "Task":
        return run_task(args["description"], args["prompt"], args["agent_type"])
    if name == "Skill":
        return run_skill(args["skill"])
    return f"未知工具:{name}"


# =============================================================================
# 主代理循环
# =============================================================================

def agent_loop(messages: list) -> list:
    """
    支持技能的主代理循环。

    与 v3 模式相同,但现在有 Skill 工具。
    当模型加载技能时,它会接收领域知识。
    """
    while True:
        response = client.messages.create(
            model=MODEL,
            system=SYSTEM,
            messages=messages,
            tools=ALL_TOOLS,
            max_tokens=8000,
        )

        tool_calls = []
        for block in response.content:
            if hasattr(block, "text"):
                print(block.text)
            if block.type == "tool_use":
                tool_calls.append(block)

        if response.stop_reason != "tool_use":
            messages.append({"role": "assistant", "content": response.content})
            return messages

        results = []
        for tc in tool_calls:
            # 为不同工具类型提供特殊显示
            if tc.name == "Task":
                print(f"\n> Task: {tc.input.get('description', '子任务')}")
            elif tc.name == "Skill":
                print(f"\n> 正在加载技能:{tc.input.get('skill', '?')}")
            else:
                print(f"\n> {tc.name}")

            output = execute_tool(tc.name, tc.input)

            # Skill 工具显示摘要,而非完整内容
            if tc.name == "Skill":
                print(f"  已加载技能({len(output)} 字符)")
            elif tc.name != "Task":
                preview = output[:200] + "..." if len(output) > 200 else output
                print(f"  {preview}")

            results.append({
                "type": "tool_result",
                "tool_use_id": tc.id,
                "content": output
            })

        messages.append({"role": "assistant", "content": response.content})
        messages.append({"role": "user", "content": results})


# =============================================================================
# 主 REPL
# =============================================================================

def main():
    print(f"Mini Claude Code v4(带技能)- {WORKDIR}")
    print(f"技能:{', '.join(SKILLS.list_skills()) or '无'}")
    print(f"代理类型:{', '.join(AGENT_TYPES.keys())}")
    print("输入 'exit' 退出。\n")

    history = []

    while True:
        try:
            user_input = input("你:").strip()
        except (EOFError, KeyboardInterrupt):
            break

        if not user_input or user_input.lower() in ("exit", "quit", "q"):
            break

        history.append({"role": "user", "content": user_input})

        try:
            agent_loop(history)
        except Exception as e:
            print(f"错误:{e}")

        print()


if __name__ == "__main__":
    main()

工具让模型行动。技能让模型知道如何行动。

评论 (0)

暂无评论,快来抢沙发吧!

91学AI

© 2026 91学AI · 按岗位学 AI 与大数据. All rights reserved.