本页目录
SkillsNote 07

06 综合案例:把 skills 串起来

学习目标

学习目标

把前面 1 到 5 章连成一个完整流程:发现 skill、读取指令、按需用资源、隔离 backend、加权限、给 subagent 配置独立能力。

场景

做一个“文档研究助手”:

  • 主 Agent 从固定 workspace 里的 /skills/ 加载 langgraph-docs skill。
  • skill 的 SKILL.md 告诉 Agent 去读 references/,必要时执行 scripts/,输出报告时用 assets/ 模板。
  • 共享 skill 库只读,防止 Agent 改规范。
  • 个人/团队记忆走 store,临时草稿走 state。
  • 自定义 researcher subagent 也能拿到同一个 skill,但权限可以单独收紧。

这条链怎么串

create_deep_agent(..., skills=["/skills/"], backend=CompositeBackend(...))
  -> SkillsMiddleware 扫到 /skills/langgraph-docs/SKILL.md
  -> 系统提示词只注入 name / description / 路径
  -> SKILL.md 说明支持资源的位置
  -> resources 按需读取
  -> permissions 禁止写共享技能
  -> subagent 显式拿到 skills 和 permissions
  -> report template 生成最终输出

最小可运行例子

代码见 ../06_comprehensive_case.py

这个例子做的不是“再讲一遍概念”,而是把概念放到同一条流程里:

  • FilesystemBackend 承载 /skills//workspace/
  • StoreBackend 承载 /memories/
  • StateBackend 作为默认临时层
  • SkillsMiddleware 负责发现和摘要
  • FilesystemPermission 保护共享 skill
  • subagents 给研究子 Agent 单独配 skill

注意:CompositeBackend 路由会剥掉 route 前缀。所以 /skills/ 路由的 FilesystemBackend root 要指向真实 workspace/skills,这样 /skills/langgraph-docs/SKILL.md 才会落到后端里的 /langgraph-docs/SKILL.md

真实 Agent 调用

如果只想最后打印完整消息链,用 invoke_and_pretty_print() 或原生 pretty_print()

from langchain.messages import HumanMessage

messages = [HumanMessage(content="请使用 langgraph-docs skill,总结 DeepAgent skills 的使用链路。")]
messages = graph.invoke({"messages": messages})
for message in messages["messages"]:
    message.pretty_print()

如果想实时看到 Agent 过程,用这章实际采用的流式方法:

from langchain.messages import HumanMessage

from deepagent_src.agent_output import stream_values_and_pretty_print

messages = [HumanMessage(content="请使用 langgraph-docs skill,总结 DeepAgent skills 的使用链路。")]
messages = stream_values_and_pretty_print(
    graph,
    {"messages": messages},
    {"configurable": {"thread_id": "skills-comprehensive-case"}},
)

在项目根目录执行:

uv run python deepagent_src/skills_teach/06_comprehensive_case.py

预期现象:

  • 终端打印完整消息链,包括 HumanMessage、模型消息和工具调用相关消息。
  • Agent 会根据 /skills/ 的 discovery 信息读取 /skills/langgraph-docs/SKILL.md
  • 如果模型按指令继续走,会读取 references/resource-map.mdassets/report-template.md
  • 最后输出 comprehensive skills case ok

注意:运行这章会调用 deepagent_src.llms.get_gpt_model(),会产生真实模型请求和可能的 API 费用。

你应该从这个案例记住什么

  1. skills 不是“再多一个目录”,而是 Agent 的按需能力入口。
  2. SKILL.md 不是资料堆,而是调度说明书。
  3. references/scripts/assets/ 是延迟加载的支持件。
  4. backend 决定文件放哪儿,permissions 决定能不能改,subagent 决定谁能用。

常见误区

最常见的误区是把前面每一章看成孤立特性。实际上 skills 的价值就是把发现、读取、执行、权限、隔离这几件事连成一条稳定链路。拆开看都懂,合起来就经常写崩,艹,所以最后一定要看综合案例。

另一个坑是权限顺序。规则是 first match wins,所以如果你先写 deny /skills/**,后面的 interrupt /skills/personal/** 就永远匹配不到。更具体的规则要放前面。

相关资源

  • 查看示例代码:deepagent_src/skills_teach/06_comprehensive_case.py
    from __future__ import annotations
    
    import os
    from pathlib import Path
    
    from deepagents import FilesystemPermission, create_deep_agent
    from deepagents.backends import CompositeBackend, FilesystemBackend, StateBackend, StoreBackend
    from deepagents.backends.utils import create_file_data
    from deepagents.middleware.filesystem import _check_fs_permission
    from deepagents.middleware.skills import SkillsMiddleware, _list_skills_with_errors
    from langchain.messages import HumanMessage
    from langgraph.checkpoint.memory import InMemorySaver
    from langgraph.store.memory import InMemoryStore
    
    from deepagent_src.agent_output import stream_values_and_pretty_print
    from deepagent_src.llms import get_gpt_model
    
    
    ROOT_DIR = Path(__file__).resolve().parent / "workspace"
    SKILLS_DIR = ROOT_DIR / "skills"
    WORKSPACE_DIR = ROOT_DIR / "workspace"
    SKILL_SOURCE = "/skills/"
    SKILL_PATH = "/skills/langgraph-docs/SKILL.md"
    MEMORY_PATH = "/memories/team-note.txt"
    
    
    def main() -> None:
        os.environ["LANGSMITH_TRACING"] = "false"
        WORKSPACE_DIR.mkdir(parents=True, exist_ok=True)
        skills_backend = FilesystemBackend(root_dir=SKILLS_DIR, virtual_mode=True)
        reports_backend = FilesystemBackend(root_dir=WORKSPACE_DIR, virtual_mode=True)
        shared_store = InMemoryStore()
        shared_store.put(
            ("skills-teach",),
            MEMORY_PATH,
            create_file_data("team prefers one-page reports"),
        )
    
        backend = CompositeBackend(
            default=StateBackend(),
            routes={
                "/skills/": skills_backend,
                "/workspace/": reports_backend,
                "/memories/": StoreBackend(
                    store=shared_store,
                    namespace=lambda _rt: ("skills-teach",),
                ),
            },
        )
    
        permissions = [
            FilesystemPermission(
                operations=["write"],
                paths=["/skills/personal/**"],
                mode="interrupt",
            ),
            FilesystemPermission(
                operations=["write"],
                paths=["/skills/**"],
                mode="deny",
            ),
        ]
    
        research_subagent = {
            "name": "researcher",
            "description": "Research with the shared LangGraph docs skill.",
            "system_prompt": "Use the configured skills when the task matches.",
            "skills": [SKILL_SOURCE],
            "permissions": permissions,
        }
    
        graph = create_deep_agent(
            model=get_gpt_model(),
            backend=backend,
            skills=[SKILL_SOURCE],
            permissions=permissions,
            subagents=[research_subagent],
            store=shared_store,
            checkpointer=InMemorySaver(),
        )
    
        skills, error = _list_skills_with_errors(backend, SKILL_SOURCE)
        middleware = SkillsMiddleware(backend=backend, sources=[SKILL_SOURCE])
        discovery_text = middleware._format_skills_list(skills)
        skill_body = skills_backend.read("/langgraph-docs/SKILL.md", limit=1000)
        reference_body = skills_backend.read(
            "/langgraph-docs/references/resource-map.md",
            limit=1000,
        )
        asset_body = skills_backend.read(
            "/langgraph-docs/assets/report-template.md",
            limit=1000,
        )
        memory_read = StoreBackend(
            store=shared_store,
            namespace=lambda _rt: ("skills-teach",),
        ).read(MEMORY_PATH)
        report_template = asset_body.file_data["content"].format(
            question="How do DeepAgent skills fit together?",
            answer="Use skills for discovery, SKILL.md for instructions, resources for support files, backend for storage, permissions for guardrails, and subagents for scoped capability.",
            sources="- SKILL.md\n- references/resource-map.md\n- assets/report-template.md",
        )
    
        assert error is None
        assert "langgraph-docs" in discovery_text
        assert "Read `/skills/langgraph-docs/SKILL.md` for full instructions" in discovery_text
        assert skill_body.file_data is not None
        assert reference_body.file_data is not None
        assert asset_body.file_data is not None
        assert memory_read.error is None
        assert report_template.startswith("# Report Template")
        assert _check_fs_permission(permissions, "write", SKILL_PATH) == "deny"
        assert _check_fs_permission(permissions, "read", SKILL_PATH) == "allow"
        assert (
            _check_fs_permission(
                permissions,
                "write",
                "/skills/personal/langgraph-docs/SKILL.md",
            )
            == "interrupt"
        )
        assert research_subagent["skills"] == [SKILL_SOURCE]
        assert graph is not None
    
        messages = [
            HumanMessage(
                content=(
                    "请使用 langgraph-docs skill,总结 DeepAgent skills 的使用链路。"
                    "要求:先读取 skill 的完整说明,再按需查看 references/resource-map.md "
                    "和 assets/report-template.md,最后用中文给出一段简短总结。"
                    "不要联网,不要写文件。"
                )
            )
        ]
        messages = stream_values_and_pretty_print(
            graph,
            {"messages": messages},
            {"configurable": {"thread_id": "skills-comprehensive-case"}},
        )
        assert messages["messages"]
    
        print("comprehensive skills case ok")
    
    
    if __name__ == "__main__":
        main()