在构建基于大语言模型(LLM)的智能体系统时,我们常常面临一个关键矛盾:一方面希望智能体掌握丰富的技能库以应对多样化需求,另一方面又受限于上下文窗口的token限制。传统全量加载方式就像每次开会都把整个档案室的资料搬进会议室——无论议题多么简单,所有文档都必须到场。
案例场景:假设我们有一个内容创作智能体,具备SEO优化、技术写作、代码审查和研究摘要四项核心技能。当用户仅请求"检查这篇博客的SEO"时:
技术细节:以GPT-4为例,32k上下文窗口的实际可用空间约28k(考虑对话历史等),当技能库增长到15-20个时,仅技能说明就可能占据50%以上的上下文预算。
渐进式披露(Progressive Disclosure)源自人机交互领域,核心思想是"按需展示信息"。在智能体架构中,我们将其实现为三级加载机制:
| 层级 | 内容 | 加载时机 | 典型体积 | 类比 |
|---|---|---|---|---|
| L1 | 技能名称+简短描述 | 初始化时 | 100token | 餐厅菜单上的菜名 |
| L2 | 完整技能指令 | 技能被选中后 | 500-5000 | 服务员讲解菜品做法 |
| L3 | 参考资料/模板 | 执行时按需 | 可变 | 厨师查看秘制酱料配方 |
技术优势:
LangGraph的图结构完美匹配渐进式加载的需求流。我们定义五个核心状态节点:
python复制class SkillsWorkflowState(TypedDict):
user_task: str # 用户原始请求
l1_skills: List[Dict] # 技能元数据仓库
selected_skill_names: List[str] # 语义匹配结果
l2_content: Dict[str, str] # 技能指令缓存
l3_content: Dict[str, str] # 动态加载的资源
token_usage: Dict # 各层token统计
final_response: str # 执行结果
状态转移流程:
code复制[START]
↓
discover_node → 扫描技能目录,提取L1元数据
↓
select_node → 基于用户任务语义匹配技能
↓
load_l2_node → 加载选中技能的完整指令
↓
execute_node → 执行技能(可能触发L3加载)
↓
[END]
python复制def discover_skills(skills_root: Path) -> List[Dict]:
skills = []
for skill_dir in skills_root.iterdir():
skill_md = skill_dir / "SKILL.md"
if not skill_md.exists():
continue
with open(skill_md, 'r', encoding='utf-8') as f:
content = f.read()
# 使用正则提取YAML frontmatter
meta = parse_frontmatter(content)
if meta.get('name'):
skills.append(meta)
return skills
优化技巧:
yaml.safe_load替代正则解析更健壮python复制SELECT_PROMPT = """你是一个技能路由助手。根据用户任务从以下技能中选择最相关的1-2个:
{skills_list}
请直接返回技能名称,多个用逗号分隔。不要解释。
用户任务:{user_task}"""
def select_skills(llm: BaseLLM, skills: List[Dict], task: str) -> List[str]:
formatted_skills = "\n".join(
f"- {s['name']}: {s['description']}"
for s in skills
)
resp = llm.invoke(
SELECT_PROMPT.format(
skills_list=formatted_skills,
user_task=task
)
)
return [
name.strip()
for name in resp.content.split(",")
if any(s['name'] == name for s in skills)
]
工程实践:
python复制def load_skill_instructions(skill_name: str) -> str:
skill_path = SKILLS_ROOT / skill_name / "SKILL.md"
if not skill_path.exists():
return ""
with open(skill_path, 'r', encoding='utf-8') as f:
content = f.read()
# 跳过frontmatter获取纯指令内容
return extract_markdown_body(content)
# 示例SKILL.md结构
"""
---
name: seo-checklist
description: SEO优化检查清单
---
# SEO检查流程
1. 关键词密度分析(使用load_skill_resource加载references/kw_guidelines.md)
2. 标题结构评估
3. 外链质量检查
"""
性能优化:
遵循Agent Skills规范,我们实现load_skill_resource工具:
python复制@tool
def load_skill_resource(skill_name: str, resource_path: str) -> str:
"""
加载技能相关资源文件。当技能指令中明确要求时调用。
参数:
- skill_name: 如'seo-checklist'
- resource_path: 相对于技能目录的路径,如'references/guidelines.md'
返回:
- 文件内容(UTF-8编码)
- 空字符串表示加载失败
"""
resource_file = SKILLS_ROOT / skill_name / resource_path
try:
return resource_file.read_text(encoding='utf-8')
except Exception:
return ""
最佳实践:
markdown复制# 代码审查规范
请先加载references/code_style.md获取代码规范,
然后按以下步骤检查...
python复制def execute_skill(llm: BaseLLM, skill_instructions: str, user_input: str) -> str:
messages = [
SystemMessage(content=f"技能指令:{skill_instructions}"),
HumanMessage(content=user_input)
]
MAX_ITERATIONS = 5
for _ in range(MAX_ITERATIONS):
response = llm_with_tools.invoke(messages)
if not response.tool_calls:
return response.content
for tool_call in response.tool_calls:
tool_name = tool_call['name']
if tool_name == "load_skill_resource":
result = load_skill_resource(**tool_call['args'])
messages.append(ToolMessage(
content=result,
tool_call_id=tool_call['id']
))
return "超过最大迭代次数"
容错机制:
python复制import tiktoken
def count_tokens(text: str, model: str = "gpt-4") -> int:
"""使用tiktoken精确计算token数量"""
enc = tiktoken.encoding_for_model(model)
return len(enc.encode(text))
在内容创作智能体上测试两种模式:
测试用例:
结果对比:
| 指标 | 全量模式 | 渐进模式 | 节省量 |
|---|---|---|---|
| 初始加载(token) | 8200 | 320 | 96% |
| 执行消耗(token) | 0 | 1800 | - |
| 总消耗(token) | 8200 | 2120 | 74% |
| 响应时间(ms) | 1200 | 1500 | +25% |
| 内存占用(MB) | 42 | 18 | 57% |
数据分析:
推荐的项目结构:
code复制skills_library/
├── seo-checklist/
│ ├── SKILL.md
│ └── references/
│ └── guidelines.md
├── blog-writer/
│ ├── SKILL.md
│ └── templates/
│ └── tech_blog.md
└── _meta/
└── manifest.json # 技能索引
冷启动优化:
缓存策略:
python复制from functools import lru_cache
@lru_cache(maxsize=32)
def cached_load_skill(skill_name: str) -> str:
return load_skill_instructions(skill_name)
监控指标:
资源加载白名单机制
python复制ALLOWED_RESOURCE_TYPES = {'.md', '.txt', '.json'}
def validate_resource_path(path: str) -> bool:
return any(path.endswith(ext) for ext in ALLOWED_RESOURCE_TYPES)
技能权限分级
内容安全检查
并行执行:
python复制def execute_parallel(skills: List[str], task: str) -> Dict[str, str]:
with ThreadPoolExecutor() as executor:
futures = {
skill: executor.submit(
execute_skill,
skill_instructions[skill],
task
)
for skill in skills
}
return {
skill: future.result()
for skill, future in futures.items()
}
串行接力:
python复制def execute_sequence(skills: List[str], initial_input: str) -> str:
intermediate = initial_input
for skill in skills:
intermediate = execute_skill(
skill_instructions[skill],
intermediate
)
return intermediate
实现技能间的输入输出类型检查:
python复制class Skill:
name: str
input_type: Type
output_type: Type
def __call__(self, input_data: Any) -> Any:
if not isinstance(input_data, self.input_type):
raise TypeError(f"Expected {self.input_type}, got {type(input_data)}")
# ...执行逻辑
使用RAG增强技能:
python复制def augment_skill(skill_name: str, knowledge_base: VectorDB):
instructions = load_skill_instructions(skill_name)
relevant_chunks = knowledge_base.query(instructions)
return f"{instructions}\n\n参考资料:\n{relevant_chunks}"
基于用户反馈的指令优化:
python复制def refine_skill(skill_name: str, feedback: str):
current = load_skill_instructions(skill_name)
refined = llm.invoke(
f"根据以下反馈优化技能指令:\n{feedback}\n\n当前指令:\n{current}"
)
save_skill_instructions(skill_name, refined.content)
这种分层加载机制不仅适用于技能系统,还可以扩展到:
在实际项目中,我们通过这种架构实现了: