技能需要
deepagents>=1.7.0。什么是技能
技能是一个文件夹目录,其中每个文件夹包含一个或多个文件,这些文件包含代理可以使用的上下文:- 一个
SKILL.md文件,包含关于该技能的说明和元数据 - 额外的脚本(可选)
- 额外的参考信息,例如文档(可选)
- 额外的资源,例如模板和其他资源(可选)
任何额外的资源(脚本、文档、模板或其他资源)必须在
SKILL.md 文件中被引用,并说明文件包含的内容以及如何使用它,以便代理能够决定何时使用它们。技能如何工作
当你创建一个深度代理时,你可以传入一个包含技能的目录列表。当代理启动时,它会读取每个SKILL.md 文件的 frontmatter。
当代理收到提示时,它会检查在完成提示时是否可以使用任何技能。如果找到匹配的提示,它会审查技能文件的其余部分。这种仅在需要时才审查技能信息的模式称为渐进式披露。
示例
你可能有一个技能文件夹,其中包含一个以特定方式使用文档站点的技能,以及另一个用于搜索 arXiv 预印本研究论文库的技能: skills/
├── langgraph-docs
│ └── SKILL.md
└── arxiv_search
├── SKILL.md
└── arxiv_search.ts # 搜索 arXiv 的代码
SKILL.md 文件始终遵循相同的模式,以 frontmatter 中的元数据开始,后跟技能的说明。
以下示例展示了一个技能,它提供了在收到提示时如何提供相关 langgraph 文档的说明:
---
name: langgraph-docs
description: Use this skill for requests related to LangGraph in order to fetch relevant documentation to provide accurate, up-to-date guidance.
---
# langgraph-docs
## 概述
本技能说明如何访问 LangGraph Python 文档,以帮助回答问题并指导实施。
## 说明
### 1. 获取文档索引
使用 fetch_url 工具读取以下 URL:
https://docs.langchain.com/llms.txt
这提供了所有可用文档的结构化列表及其描述。
### 2. 选择相关文档
根据问题,从索引中识别 2-4 个最相关的文档 URL。优先考虑:
- 针对实施问题的具体操作指南
- 针对理解问题的核心概念页面
- 针对端到端示例的教程
- 针对 API 详情的参考文档
### 3. 获取选定的文档
使用 fetch_url 工具读取选定的文档 URL。
### 4. 提供准确的指导
阅读文档后,完成用户的请求。
重要有关编写技能文件时的约束和最佳实践,请参阅完整的 Agent Skills 规范。值得注意的是:
description字段如果超过 1024 个字符,将被截断。- 在 Deep Agents 中,
SKILL.md文件必须小于 10 MB。超过此限制的文件在技能加载时将被跳过。
完整示例
以下示例展示了一个使用所有可用 frontmatter 字段的SKILL.md 文件:
---
name: langgraph-docs
description: Use this skill for requests related to LangGraph in order to fetch relevant documentation to provide accurate, up-to-date guidance.
license: MIT
compatibility: Requires internet access for fetching documentation URLs
metadata:
author: langchain
version: "1.0"
allowed-tools: fetch_url
---
# langgraph-docs
## 概述
本技能说明如何访问 LangGraph Python 文档,以帮助回答问题并指导实施。
## 说明
### 1. 获取文档索引
使用 fetch_url 工具读取以下 URL:
https://docs.langchain.com/llms.txt
这提供了所有可用文档的结构化列表及其描述。
### 2. 选择相关文档
根据问题,从索引中识别 2-4 个最相关的文档 URL。优先考虑:
- 针对实施问题的具体操作指南
- 针对理解问题的核心概念页面
- 针对端到端示例的教程
- 针对 API 详情的参考文档
### 3. 获取选定的文档
使用 fetch_url 工具读取选定的文档 URL。
### 4. 提供准确的指导
阅读文档后,完成用户的请求。
用法
在创建深度代理时传入技能目录:- 状态后端
- 存储后端
- 文件系统后端
import { createDeepAgent, type FileData } from "deepagents";
import { MemorySaver } from "@langchain/langgraph";
const checkpointer = new MemorySaver();
function createFileData(content: string): FileData {
const now = new Date().toISOString();
return {
content: content.split("\n"),
created_at: now,
modified_at: now,
};
}
const skillsFiles: Record<string, FileData> = {};
const skillUrl =
"https://raw.githubusercontent.com/langchain-ai/deepagentsjs/refs/heads/main/examples/skills/langgraph-docs/SKILL.md";
const response = await fetch(skillUrl);
const skillContent = await response.text();
skillsFiles["/skills/langgraph-docs/SKILL.md"] = createFileData(skillContent);
const agent = await createDeepAgent({
checkpointer,
// 重要:deepagents 技能源路径是相对于后端根目录的虚拟(POSIX)路径。
skills: ["/skills/"],
});
const config = {
configurable: {
thread_id: `thread-${Date.now()}`,
},
};
const result = await agent.invoke(
{
messages: [
{
role: "user",
content: "什么是 langraph?如果可用,请使用 langgraph-docs 技能。",
},
],
files: skillsFiles,
},
config,
);
import { createDeepAgent, StoreBackend, type FileData } from "deepagents";
import {
InMemoryStore,
MemorySaver,
} from "@langchain/langgraph";
const checkpointer = new MemorySaver();
const store = new InMemoryStore();
function createFileData(content: string): FileData {
const now = new Date().toISOString();
return {
content: content.split("\n"),
created_at: now,
modified_at: now,
};
}
const skillUrl =
"https://raw.githubusercontent.com/langchain-ai/deepagentsjs/refs/heads/main/examples/skills/langgraph-docs/SKILL.md";
const response = await fetch(skillUrl);
const skillContent = await response.text();
const fileData = createFileData(skillContent);
await store.put(["filesystem"], "/skills/langgraph-docs/SKILL.md", fileData);
const agent = await createDeepAgent({
backend: new StoreBackend(),
store: store,
checkpointer,
// 重要:deepagents 技能源路径是相对于后端根目录的虚拟(POSIX)路径。
skills: ["/skills/"],
});
const config = {
recursionLimit: 50,
configurable: {
thread_id: `thread-${Date.now()}`,
},
};
const result = await agent.invoke(
{
messages: [
{
role: "user",
content: "什么是 langraph?如果可用,请使用 langgraph-docs 技能。",
},
],
},
config,
);
import { createDeepAgent, FilesystemBackend } from "deepagents";
import { MemorySaver } from "@langchain/langgraph";
const checkpointer = new MemorySaver();
const backend = new FilesystemBackend({ rootDir: process.cwd() });
const agent = await createDeepAgent({
backend,
skills: ["./examples/skills/"],
interruptOn: {
read_file: true,
write_file: true,
delete_file: true,
},
checkpointer, // 必需!
});
const config = {
configurable: {
thread_id: `thread-${Date.now()}`,
},
};
const result = await agent.invoke(
{
messages: [
{
role: "user",
content: "什么是 langraph?如果可用,请使用 langgraph-docs 技能。",
},
],
},
config,
);
list[str]
技能源路径列表。路径必须使用正斜杠指定,并且相对于后端的根目录。
- 如果省略,则不加载任何技能。
- 使用
StateBackend(默认)时,通过invoke(files={...})提供技能文件。 - 使用
FilesystemBackend时,技能从相对于后端root_dir的磁盘加载。
SDK 仅加载你在
skills 中传入的源。它不会自动扫描 CLI 目录,如 ~/.deepagents/... 或 ~/.agents/...。有关 CLI 存储约定,请参阅 应用数据。在 SDK 中模拟 CLI 源顺序
在 SDK 中模拟 CLI 源顺序
如果你想在 SDK 代码中实现类似 CLI 的分层,请按从低到高的优先级顺序显式传入所有所需的源:然后在创建代理时将该有序列表作为
[
"<user-home>/.deepagents/{agent}/skills/",
"<user-home>/.agents/skills/",
"<project-root>/.deepagents/skills/",
"<project-root>/.agents/skills/",
]
skills 传入。源优先级
当多个技能源包含同名技能时,skills 数组中列出较后的源中的技能优先(后者优先)。这允许你分层来自不同来源的技能。
// 如果两个源都包含名为 "web-search" 的技能,
// 则来自 "/skills/project/" 的那个优先(最后加载)。
const agent = await createDeepAgent({
skills: ["/skills/user/", "/skills/project/"],
...
});
子代理的技能
当你使用子代理时,你可以配置每种类型可以访问哪些技能:- 通用子代理:当你向
create_deep_agent传入skills时,会自动继承主代理的技能。无需额外配置。 - 自定义子代理:不继承主代理的技能。在每个子代理定义中添加一个
skills参数,指定该子代理的技能源路径。
const researchSubagent = {
name: "researcher",
description: "Research assistant with specialized skills",
systemPrompt: "You are a researcher.",
tools: [webSearch],
skills: ["/skills/research/", "/skills/web-search/"], // 子代理特定的技能
};
const agent = await createDeepAgent({
model: "google_genai:gemini-3.1-pro-preview",
skills: ["/skills/main/"], // 主代理和通用子代理获得这些技能
subagents: [researchSubagent], // 研究员只获得自己的技能
});
代理看到的内容
配置技能后,代理的系统提示中会注入一个“技能系统”部分。代理使用此信息遵循三步流程:- 匹配—当用户提示到达时,代理检查是否有任何技能的描述与任务匹配。
- 读取—如果适用某个技能,代理会使用其技能列表中显示的路径读取完整的
SKILL.md文件。 - 执行—代理遵循技能的说明,并根据需要访问任何支持文件(脚本、模板、参考文档)。
在
SKILL.md frontmatter 中编写清晰、具体的描述。代理仅根据描述决定是否使用技能——详细的描述能带来更好的技能匹配。在沙箱中执行技能脚本
技能可以包含与SKILL.md 文件一起的脚本,例如,执行搜索或数据转换的 Python 文件。代理可以从任何后端_读取_这些脚本,但要_执行_它们,代理需要访问 shell——只有沙箱后端提供此功能。
当你使用 CompositeBackend 将技能路由到 StoreBackend 进行持久化,同时使用沙箱作为默认后端时,技能文件存储在存储中,而不是代码运行的沙箱中。为了让沙箱能够使用这些脚本,你必须使用自定义中间件在代理启动前将技能脚本上传到沙箱:
import { readFile, readdir } from "node:fs/promises";
import { join, posix, relative, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { createMiddleware } from "langchain";
import {
CompositeBackend,
createDeepAgent,
type FileData,
StoreBackend,
} from "deepagents";
import { InMemoryStore } from "@langchain/langgraph";
import { DaytonaSandbox } from "@langchain/daytona";
/** 每个用户相同的技能包:一个共享的存储命名空间。 */
const SKILLS_SHARED_NAMESPACE = ["skills", "builtin"] as const;
function createFileData(content: string): FileData {
const now = new Date().toISOString();
return {
content: content.split("\n"),
created_at: now,
modified_at: now,
};
}
function normalizeSkillsStoreKey(key: string): string {
const k = String(key);
if (k.includes("..") || /[*?]/.test(k)) {
throw new Error(`Invalid key: ${key}`);
}
return k.startsWith("/") ? k : `/${k}`;
}
async function walkFiles(dir: string): Promise<string[]> {
const entries = await readdir(dir, { withFileTypes: true });
const files: string[] = [];
for (const entry of entries) {
const fullPath = join(dir, entry.name);
if (entry.isDirectory()) {
files.push(...(await walkFiles(fullPath)));
} else if (entry.isFile()) {
files.push(fullPath);
}
}
return files.sort((a, b) => a.localeCompare(b));
}
/** 从磁盘加载规范技能文件到共享存储命名空间(部署时运行一次)。
* 您可以从任何来源(本地文件系统、远程 URL 等)检索技能。
*/
async function seedSkillStore(store: InMemoryStore) {
const moduleDir = resolve(fileURLToPath(new URL(".", import.meta.url)));
const skillsDir = resolve(moduleDir, "skills");
const filePaths = await walkFiles(skillsDir);
for (const filePath of filePaths) {
const rel = relative(skillsDir, filePath);
// StoreBackend 的键是*相对于路由后端根目录*的路径。
// CompositeBackend 在委托前会剥离路由前缀 (`/skills/`),
// 因此存储键应类似于 "/<skillname>/SKILL.md"。
const key = `/${posix.normalize(rel.split("\\").join("/"))}`;
const content = await readFile(filePath, "utf8");
await store.put([...SKILLS_SHARED_NAMESPACE], key, createFileData(content));
}
}
/** 在每次代理运行前,将共享技能文件从存储复制到沙箱中。 */
function createSkillSandboxSyncMiddleware(backend: CompositeBackend) {
return createMiddleware({
name: "SkillSandboxSyncMiddleware",
beforeAgent: async (state, runtime) => {
const store = (runtime as any).store;
if (!store) {
throw new Error(
"Store is required for syncing skills into the sandbox. " +
"Pass `store` to createDeepAgent and ensure your runtime provides it.",
);
}
const encoder = new TextEncoder();
const files: Array<[string, Uint8Array]> = [];
for (const item of await store.search([...SKILLS_SHARED_NAMESPACE])) {
const normalized = normalizeSkillsStoreKey(String(item.key));
const data = item.value as FileData;
// CompositeBackend 路由路径并将批量上传到正确的后端。
files.push([
`/skills${normalized}`,
encoder.encode(data.content.join("\n")),
]);
}
if (files.length > 0) await backend.uploadFiles(files);
return state;
},
});
}
async function main() {
const store = new InMemoryStore();
await seedSkillStore(store);
const sandbox = await DaytonaSandbox.create({
language: "python",
timeout: 300,
});
const backend = new CompositeBackend(sandbox, {
"/skills/": new StoreBackend({
store,
namespace: () => [...SKILLS_SHARED_NAMESPACE],
} as any),
});
try {
const agent = await createDeepAgent({
model: "google-genai:gemini-3.1-pro-preview",
backend,
skills: ["/skills/"],
store,
middleware: [createSkillSandboxSyncMiddleware(backend)],
});
} finally {
await sandbox.close();
}
}
main().catch((err) => {
console.error(err);
process.exitCode = 1;
});
import { readFile, readdir } from "node:fs/promises";
import { join, posix, relative, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { createMiddleware } from "langchain";
import {
CompositeBackend,
createDeepAgent,
type FileData,
StoreBackend,
} from "deepagents";
import { InMemoryStore } from "@langchain/langgraph";
import { DaytonaSandbox } from "@langchain/daytona";
/** 每个用户相同的技能包:一个共享的存储命名空间。 */
const SKILLS_SHARED_NAMESPACE = ["skills", "builtin"] as const;
function createFileData(content: string): FileData {
const now = new Date().toISOString();
return {
content: content.split("\n"),
created_at: now,
modified_at: now,
};
}
function normalizeSkillsStoreKey(key: string): string {
const k = String(key);
if (k.includes("..") || /[*?]/.test(k)) {
throw new Error(`Invalid key: ${key}`);
}
return k.startsWith("/") ? k : `/${k}`;
}
async function walkFiles(dir: string): Promise<string[]> {
const entries = await readdir(dir, { withFileTypes: true });
const files: string[] = [];
for (const entry of entries) {
const fullPath = join(dir, entry.name);
if (entry.isDirectory()) {
files.push(...(await walkFiles(fullPath)));
} else if (entry.isFile()) {
files.push(fullPath);
}
}
return files.sort((a, b) => a.localeCompare(b));
}
/** 从磁盘加载规范技能文件到共享存储命名空间(部署时运行一次)。
* 您可以从任何来源(本地文件系统、远程 URL 等)检索技能。
*/
async function seedSkillStore(store: InMemoryStore) {
const moduleDir = resolve(fileURLToPath(new URL(".", import.meta.url)));
const skillsDir = resolve(moduleDir, "skills");
const filePaths = await walkFiles(skillsDir);
for (const filePath of filePaths) {
const rel = relative(skillsDir, filePath);
// StoreBackend 的键是*相对于路由后端根目录*的路径。
// CompositeBackend 在委托前会剥离路由前缀 (`/skills/`),
// 因此存储键应类似于 "/<skillname>/SKILL.md"。
const key = `/${posix.normalize(rel.split("\\").join("/"))}`;
const content = await readFile(filePath, "utf8");
await store.put([...SKILLS_SHARED_NAMESPACE], key, createFileData(content));
}
}
/** 在每次代理运行前,将共享技能文件从存储复制到沙箱中。 */
function createSkillSandboxSyncMiddleware(backend: CompositeBackend) {
return createMiddleware({
name: "SkillSandboxSyncMiddleware",
beforeAgent: async (state, runtime) => {
const store = (runtime as any).store;
if (!store) {
throw new Error(
"Store is required for syncing skills into the sandbox. " +
"Pass `store` to createDeepAgent and ensure your runtime provides it.",
);
}
const encoder = new TextEncoder();
const files: Array<[string, Uint8Array]> = [];
for (const item of await store.search([...SKILLS_SHARED_NAMESPACE])) {
const normalized = normalizeSkillsStoreKey(String(item.key));
const data = item.value as FileData;
// CompositeBackend 路由路径并将批量上传到正确的后端。
files.push([
`/skills${normalized}`,
encoder.encode(data.content.join("\n")),
]);
}
if (files.length > 0) await backend.uploadFiles(files);
return state;
},
});
}
async function main() {
const store = new InMemoryStore();
await seedSkillStore(store);
const sandbox = await DaytonaSandbox.create({
language: "python",
timeout: 300,
});
const backend = new CompositeBackend(sandbox, {
"/skills/": new StoreBackend({
store,
namespace: () => [...SKILLS_SHARED_NAMESPACE],
} as any),
});
try {
const agent = await createDeepAgent({
model: "openai:gpt-5.4",
backend,
skills: ["/skills/"],
store,
middleware: [createSkillSandboxSyncMiddleware(backend)],
});
} finally {
await sandbox.close();
}
}
main().catch((err) => {
console.error(err);
process.exitCode = 1;
});
import { readFile, readdir } from "node:fs/promises";
import { join, posix, relative, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { createMiddleware } from "langchain";
import {
CompositeBackend,
createDeepAgent,
type FileData,
StoreBackend,
} from "deepagents";
import { InMemoryStore } from "@langchain/langgraph";
import { DaytonaSandbox } from "@langchain/daytona";
/** 每个用户相同的技能包:一个共享的存储命名空间。 */
const SKILLS_SHARED_NAMESPACE = ["skills", "builtin"] as const;
function createFileData(content: string): FileData {
const now = new Date().toISOString();
return {
content: content.split("\n"),
created_at: now,
modified_at: now,
};
}
function normalizeSkillsStoreKey(key: string): string {
const k = String(key);
if (k.includes("..") || /[*?]/.test(k)) {
throw new Error(`Invalid key: ${key}`);
}
return k.startsWith("/") ? k : `/${k}`;
}
async function walkFiles(dir: string): Promise<string[]> {
const entries = await readdir(dir, { withFileTypes: true });
const files: string[] = [];
for (const entry of entries) {
const fullPath = join(dir, entry.name);
if (entry.isDirectory()) {
files.push(...(await walkFiles(fullPath)));
} else if (entry.isFile()) {
files.push(fullPath);
}
}
return files.sort((a, b) => a.localeCompare(b));
}
/** 从磁盘加载规范技能文件到共享存储命名空间(部署时运行一次)。
* 您可以从任何来源(本地文件系统、远程 URL 等)检索技能。
*/
async function seedSkillStore(store: InMemoryStore) {
const moduleDir = resolve(fileURLToPath(new URL(".", import.meta.url)));
const skillsDir = resolve(moduleDir, "skills");
const filePaths = await walkFiles(skillsDir);
for (const filePath of filePaths) {
const rel = relative(skillsDir, filePath);
// StoreBackend 的键是*相对于路由后端根目录*的路径。
// CompositeBackend 在委托前会剥离路由前缀 (`/skills/`),
// 因此存储键应类似于 "/<skillname>/SKILL.md"。
const key = `/${posix.normalize(rel.split("\\").join("/"))}`;
const content = await readFile(filePath, "utf8");
await store.put([...SKILLS_SHARED_NAMESPACE], key, createFileData(content));
}
}
/** 在每次代理运行前,将共享技能文件从存储复制到沙箱中。 */
function createSkillSandboxSyncMiddleware(backend: CompositeBackend) {
return createMiddleware({
name: "SkillSandboxSyncMiddleware",
beforeAgent: async (state, runtime) => {
const store = (runtime as any).store;
if (!store) {
throw new Error(
"Store is required for syncing skills into the sandbox. " +
"Pass `store` to createDeepAgent and ensure your runtime provides it.",
);
}
const encoder = new TextEncoder();
const files: Array<[string, Uint8Array]> = [];
for (const item of await store.search([...SKILLS_SHARED_NAMESPACE])) {
const normalized = normalizeSkillsStoreKey(String(item.key));
const data = item.value as FileData;
// CompositeBackend 路由路径并将批量上传到正确的后端。
files.push([
`/skills${normalized}`,
encoder.encode(data.content.join("\n")),
]);
}
if (files.length > 0) await backend.uploadFiles(files);
return state;
},
});
}
async function main() {
const store = new InMemoryStore();
await seedSkillStore(store);
const sandbox = await DaytonaSandbox.create({
language: "python",
timeout: 300,
});
const backend = new CompositeBackend(sandbox, {
"/skills/": new StoreBackend({
store,
namespace: () => [...SKILLS_SHARED_NAMESPACE],
} as any),
});
try {
const agent = await createDeepAgent({
model: "anthropic:claude-sonnet-4-6",
backend,
skills: ["/skills/"],
store,
middleware: [createSkillSandboxSyncMiddleware(backend)],
});
} finally {
await sandbox.close();
}
}
main().catch((err) => {
console.error(err);
process.exitCode = 1;
});
import { readFile, readdir } from "node:fs/promises";
import { join, posix, relative, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { createMiddleware } from "langchain";
import {
CompositeBackend,
createDeepAgent,
type FileData,
StoreBackend,
} from "deepagents";
import { InMemoryStore } from "@langchain/langgraph";
import { DaytonaSandbox } from "@langchain/daytona";
/** 每个用户相同的技能包:一个共享的存储命名空间。 */
const SKILLS_SHARED_NAMESPACE = ["skills", "builtin"] as const;
function createFileData(content: string): FileData {
const now = new Date().toISOString();
return {
content: content.split("\n"),
created_at: now,
modified_at: now,
};
}
function normalizeSkillsStoreKey(key: string): string {
const k = String(key);
if (k.includes("..") || /[*?]/.test(k)) {
throw new Error(`Invalid key: ${key}`);
}
return k.startsWith("/") ? k : `/${k}`;
}
async function walkFiles(dir: string): Promise<string[]> {
const entries = await readdir(dir, { withFileTypes: true });
const files: string[] = [];
for (const entry of entries) {
const fullPath = join(dir, entry.name);
if (entry.isDirectory()) {
files.push(...(await walkFiles(fullPath)));
} else if (entry.isFile()) {
files.push(fullPath);
}
}
return files.sort((a, b) => a.localeCompare(b));
}
/** 从磁盘加载规范技能文件到共享存储命名空间(部署时运行一次)。
* 您可以从任何来源(本地文件系统、远程 URL 等)检索技能。
*/
async function seedSkillStore(store: InMemoryStore) {
const moduleDir = resolve(fileURLToPath(new URL(".", import.meta.url)));
const skillsDir = resolve(moduleDir, "skills");
const filePaths = await walkFiles(skillsDir);
for (const filePath of filePaths) {
const rel = relative(skillsDir, filePath);
// StoreBackend 的键是*相对于路由后端根目录*的路径。
// CompositeBackend 在委托前会剥离路由前缀 (`/skills/`),
// 因此存储键应类似于 "/<skillname>/SKILL.md"。
const key = `/${posix.normalize(rel.split("\\").join("/"))}`;
const content = await readFile(filePath, "utf8");
await store.put([...SKILLS_SHARED_NAMESPACE], key, createFileData(content));
}
}
/** 在每次代理运行前,将共享技能文件从存储复制到沙箱中。 */
function createSkillSandboxSyncMiddleware(backend: CompositeBackend) {
return createMiddleware({
name: "SkillSandboxSyncMiddleware",
beforeAgent: async (state, runtime) => {
const store = (runtime as any).store;
if (!store) {
throw new Error(
"Store is required for syncing skills into the sandbox. " +
"Pass `store` to createDeepAgent and ensure your runtime provides it.",
);
}
const encoder = new TextEncoder();
const files: Array<[string, Uint8Array]> = [];
for (const item of await store.search([...SKILLS_SHARED_NAMESPACE])) {
const normalized = normalizeSkillsStoreKey(String(item.key));
const data = item.value as FileData;
// CompositeBackend 路由路径并将批量上传到正确的后端。
files.push([
`/skills${normalized}`,
encoder.encode(data.content.join("\n")),
]);
}
if (files.length > 0) await backend.uploadFiles(files);
return state;
},
});
}
async function main() {
const store = new InMemoryStore();
await seedSkillStore(store);
const sandbox = await DaytonaSandbox.create({
language: "python",
timeout: 300,
});
const backend = new CompositeBackend(sandbox, {
"/skills/": new StoreBackend({
store,
namespace: () => [...SKILLS_SHARED_NAMESPACE],
} as any),
});
try {
const agent = await createDeepAgent({
model: "openrouter:anthropic/claude-sonnet-4-6",
backend,
skills: ["/skills/"],
store,
middleware: [createSkillSandboxSyncMiddleware(backend)],
});
} finally {
await sandbox.close();
}
}
main().catch((err) => {
console.error(err);
process.exitCode = 1;
});
import { readFile, readdir } from "node:fs/promises";
import { join, posix, relative, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { createMiddleware } from "langchain";
import {
CompositeBackend,
createDeepAgent,
type FileData,
StoreBackend,
} from "deepagents";
import { InMemoryStore } from "@langchain/langgraph";
import { DaytonaSandbox } from "@langchain/daytona";
/** 每个用户相同的技能包:一个共享的存储命名空间。 */
const SKILLS_SHARED_NAMESPACE = ["skills", "builtin"] as const;
function createFileData(content: string): FileData {
const now = new Date().toISOString();
return {
content: content.split("\n"),
created_at: now,
modified_at: now,
};
}
function normalizeSkillsStoreKey(key: string): string {
const k = String(key);
if (k.includes("..") || /[*?]/.test(k)) {
throw new Error(`Invalid key: ${key}`);
}
return k.startsWith("/") ? k : `/${k}`;
}
async function walkFiles(dir: string): Promise<string[]> {
const entries = await readdir(dir, { withFileTypes: true });
const files: string[] = [];
for (const entry of entries) {
const fullPath = join(dir, entry.name);
if (entry.isDirectory()) {
files.push(...(await walkFiles(fullPath)));
} else if (entry.isFile()) {
files.push(fullPath);
}
}
return files.sort((a, b) => a.localeCompare(b));
}
/** 从磁盘加载规范技能文件到共享存储命名空间(部署时运行一次)。
* 您可以从任何来源(本地文件系统、远程 URL 等)检索技能。
*/
async function seedSkillStore(store: InMemoryStore) {
const moduleDir = resolve(fileURLToPath(new URL(".", import.meta.url)));
const skillsDir = resolve(moduleDir, "skills");
const filePaths = await walkFiles(skillsDir);
for (const filePath of filePaths) {
const rel = relative(skillsDir, filePath);
// StoreBackend 的键是*相对于路由后端根目录*的路径。
// CompositeBackend 在委托前会剥离路由前缀 (`/skills/`),
// 因此存储键应类似于 "/<skillname>/SKILL.md"。
const key = `/${posix.normalize(rel.split("\\").join("/"))}`;
const content = await readFile(filePath, "utf8");
await store.put([...SKILLS_SHARED_NAMESPACE], key, createFileData(content));
}
}
/** 在每次代理运行前,将共享技能文件从存储复制到沙箱中。 */
function createSkillSandboxSyncMiddleware(backend: CompositeBackend) {
return createMiddleware({
name: "SkillSandboxSyncMiddleware",
beforeAgent: async (state, runtime) => {
const store = (runtime as any).store;
if (!store) {
throw new Error(
"Store is required for syncing skills into the sandbox. " +
"Pass `store` to createDeepAgent and ensure your runtime provides it.",
);
}
const encoder = new TextEncoder();
const files: Array<[string, Uint8Array]> = [];
for (const item of await store.search([...SKILLS_SHARED_NAMESPACE])) {
const normalized = normalizeSkillsStoreKey(String(item.key));
const data = item.value as FileData;
// CompositeBackend 路由路径并将批量上传到正确的后端。
files.push([
`/skills${normalized}`,
encoder.encode(data.content.join("\n")),
]);
}
if (files.length > 0) await backend.uploadFiles(files);
return state;
},
});
}
async function main() {
const store = new InMemoryStore();
await seedSkillStore(store);
const sandbox = await DaytonaSandbox.create({
language: "python",
timeout: 300,
});
const backend = new CompositeBackend(sandbox, {
"/skills/": new StoreBackend({
store,
namespace: () => [...SKILLS_SHARED_NAMESPACE],
} as any),
});
try {
const agent = await createDeepAgent({
model: "fireworks:accounts/fireworks/models/qwen3p5-397b-a17b",
backend,
skills: ["/skills/"],
store,
middleware: [createSkillSandboxSyncMiddleware(backend)],
});
} finally {
await sandbox.close();
}
}
main().catch((err) => {
console.error(err);
process.exitCode = 1;
});
import { readFile, readdir } from "node:fs/promises";
import { join, posix, relative, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { createMiddleware } from "langchain";
import {
CompositeBackend,
createDeepAgent,
type FileData,
StoreBackend,
} from "deepagents";
import { InMemoryStore } from "@langchain/langgraph";
import { DaytonaSandbox } from "@langchain/daytona";
/** 每个用户相同的技能包:一个共享的存储命名空间。 */
const SKILLS_SHARED_NAMESPACE = ["skills", "builtin"] as const;
function createFileData(content: string): FileData {
const now = new Date().toISOString();
return {
content: content.split("\n"),
created_at: now,
modified_at: now,
};
}
function normalizeSkillsStoreKey(key: string): string {
const k = String(key);
if (k.includes("..") || /[*?]/.test(k)) {
throw new Error(`Invalid key: ${key}`);
}
return k.startsWith("/") ? k : `/${k}`;
}
async function walkFiles(dir: string): Promise<string[]> {
const entries = await readdir(dir, { withFileTypes: true });
const files: string[] = [];
for (const entry of entries) {
const fullPath = join(dir, entry.name);
if (entry.isDirectory()) {
files.push(...(await walkFiles(fullPath)));
} else if (entry.isFile()) {
files.push(fullPath);
}
}
return files.sort((a, b) => a.localeCompare(b));
}
/** 从磁盘加载规范技能文件到共享存储命名空间(部署时运行一次)。
* 您可以从任何来源(本地文件系统、远程 URL 等)检索技能。
*/
async function seedSkillStore(store: InMemoryStore) {
const moduleDir = resolve(fileURLToPath(new URL(".", import.meta.url)));
const skillsDir = resolve(moduleDir, "skills");
const filePaths = await walkFiles(skillsDir);
for (const filePath of filePaths) {
const rel = relative(skillsDir, filePath);
// StoreBackend 的键是*相对于路由后端根目录*的路径。
// CompositeBackend 在委托前会剥离路由前缀 (`/skills/`),
// 因此存储键应类似于 "/<skillname>/SKILL.md"。
const key = `/${posix.normalize(rel.split("\\").join("/"))}`;
const content = await readFile(filePath, "utf8");
await store.put([...SKILLS_SHARED_NAMESPACE], key, createFileData(content));
}
}
/** 在每次代理运行前,将共享技能文件从存储复制到沙箱中。 */
function createSkillSandboxSyncMiddleware(backend: CompositeBackend) {
return createMiddleware({
name: "SkillSandboxSyncMiddleware",
beforeAgent: async (state, runtime) => {
const store = (runtime as any).store;
if (!store) {
throw new Error(
"Store is required for syncing skills into the sandbox. " +
"Pass `store` to createDeepAgent and ensure your runtime provides it.",
);
}
const encoder = new TextEncoder();
const files: Array<[string, Uint8Array]> = [];
for (const item of await store.search([...SKILLS_SHARED_NAMESPACE])) {
const normalized = normalizeSkillsStoreKey(String(item.key));
const data = item.value as FileData;
// CompositeBackend 路由路径并将批量上传到正确的后端。
files.push([
`/skills${normalized}`,
encoder.encode(data.content.join("\n")),
]);
}
if (files.length > 0) await backend.uploadFiles(files);
return state;
},
});
}
async function main() {
const store = new InMemoryStore();
await seedSkillStore(store);
const sandbox = await DaytonaSandbox.create({
language: "python",
timeout: 300,
});
const backend = new CompositeBackend(sandbox, {
"/skills/": new StoreBackend({
store,
namespace: () => [...SKILLS_SHARED_NAMESPACE],
} as any),
});
try {
const agent = await createDeepAgent({
model: "baseten:zai-org/GLM-5",
backend,
skills: ["/skills/"],
store,
middleware: [createSkillSandboxSyncMiddleware(backend)],
});
} finally {
await sandbox.close();
}
}
main().catch((err) => {
console.error(err);
process.exitCode = 1;
});
import { readFile, readdir } from "node:fs/promises";
import { join, posix, relative, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { createMiddleware } from "langchain";
import {
CompositeBackend,
createDeepAgent,
type FileData,
StoreBackend,
} from "deepagents";
import { InMemoryStore } from "@langchain/langgraph";
import { DaytonaSandbox } from "@langchain/daytona";
/** 每个用户相同的技能包:一个共享的存储命名空间。 */
const SKILLS_SHARED_NAMESPACE = ["skills", "builtin"] as const;
function createFileData(content: string): FileData {
const now = new Date().toISOString();
return {
content: content.split("\n"),
created_at: now,
modified_at: now,
};
}
function normalizeSkillsStoreKey(key: string): string {
const k = String(key);
if (k.includes("..") || /[*?]/.test(k)) {
throw new Error(`Invalid key: ${key}`);
}
return k.startsWith("/") ? k : `/${k}`;
}
async function walkFiles(dir: string): Promise<string[]> {
const entries = await readdir(dir, { withFileTypes: true });
const files: string[] = [];
for (const entry of entries) {
const fullPath = join(dir, entry.name);
if (entry.isDirectory()) {
files.push(...(await walkFiles(fullPath)));
} else if (entry.isFile()) {
files.push(fullPath);
}
}
return files.sort((a, b) => a.localeCompare(b));
}
/** 从磁盘加载规范技能文件到共享存储命名空间(部署时运行一次)。
* 您可以从任何来源(本地文件系统、远程 URL 等)检索技能。
*/
async function seedSkillStore(store: InMemoryStore) {
const moduleDir = resolve(fileURLToPath(new URL(".", import.meta.url)));
const skillsDir = resolve(moduleDir, "skills");
const filePaths = await walkFiles(skillsDir);
for (const filePath of filePaths) {
const rel = relative(skillsDir, filePath);
// StoreBackend 的键是*相对于路由后端根目录*的路径。
// CompositeBackend 在委托前会剥离路由前缀 (`/skills/`),
// 因此存储键应类似于 "/<skillname>/SKILL.md"。
const key = `/${posix.normalize(rel.split("\\").join("/"))}`;
const content = await readFile(filePath, "utf8");
await store.put([...SKILLS_SHARED_NAMESPACE], key, createFileData(content));
}
}
/** 在每次代理运行前,将共享技能文件从存储复制到沙箱中。 */
function createSkillSandboxSyncMiddleware(backend: CompositeBackend) {
return createMiddleware({
name: "SkillSandboxSyncMiddleware",
beforeAgent: async (state, runtime) => {
const store = (runtime as any).store;
if (!store) {
throw new Error(
"Store is required for syncing skills into the sandbox. " +
"Pass `store` to createDeepAgent and ensure your runtime provides it.",
);
}
const encoder = new TextEncoder();
const files: Array<[string, Uint8Array]> = [];
for (const item of await store.search([...SKILLS_SHARED_NAMESPACE])) {
const normalized = normalizeSkillsStoreKey(String(item.key));
const data = item.value as FileData;
// CompositeBackend 路由路径并将批量上传到正确的后端。
files.push([
`/skills${normalized}`,
encoder.encode(data.content.join("\n")),
]);
}
if (files.length > 0) await backend.uploadFiles(files);
return state;
},
});
}
async function main() {
const store = new InMemoryStore();
await seedSkillStore(store);
const sandbox = await DaytonaSandbox.create({
language: "python",
timeout: 300,
});
const backend = new CompositeBackend(sandbox, {
"/skills/": new StoreBackend({
store,
namespace: () => [...SKILLS_SHARED_NAMESPACE],
} as any),
});
try {
const agent = await createDeepAgent({
model: "ollama:devstral-2",
backend,
skills: ["/skills/"],
store,
middleware: [createSkillSandboxSyncMiddleware(backend)],
});
} finally {
await sandbox.close();
}
}
main().catch((err) => {
console.error(err);
process.exitCode = 1;
});
beforeAgent 钩子在每次代理调用前运行,从该共享命名空间读取技能文件并将其上传到沙箱文件系统。同步后,代理可以使用 execute 工具执行脚本,就像沙箱中的任何其他文件一样。
有关更完整的示例,该示例还双向同步记忆,请参阅使用自定义中间件同步技能和记忆。
技能与记忆
技能和记忆(AGENTS.md 文件)服务于不同的目的:
| 技能 | 记忆 | |
|---|---|---|
| 目的 | 通过渐进式披露发现的按需能力 | 启动时始终加载的持久上下文 |
| 加载 | 仅在代理确定相关性时读取 | 始终注入系统提示 |
| 格式 | 命名目录中的 SKILL.md | AGENTS.md 文件 |
| 分层 | 用户 → 项目(后者优先) | 用户 → 项目(合并) |
| 使用场景 | 说明是任务特定的且可能很大 | 上下文始终相关(项目约定、偏好) |
何时使用技能和工具
以下是使用工具和技能的一些通用指南:- 当有大量上下文时使用技能,以减少系统提示中的令牌数量。
- 使用技能将能力捆绑成更大的操作,并提供超出单个工具描述的额外上下文。
- 如果代理无法访问文件系统,则使用工具。
将这些文档通过 MCP 连接到 Claude、VSCode 等,以获取实时答案。

