feat(sdlc): 技能分层导入统一——所有角色技能只注目录层 + load_skill 按需加载全文/子文件(含 PM/QC),对齐 Hermes 分层机制

- _build_role_skills_block: 删除 role scope 全量注入正文特例,所有技能统一目录层
- 新增 _load_skill_by_name: load_skill 工具后端,支持 name 加载全文 + file_path 加载 references/scripts/templates 子文件
- AGENT_TOOLS/PM_SYSTEM_PROMPT/QC_SYSTEM_PROMPT: 加 load_skill 工具 + 技能使用引导
- role/pm/qc 三个 run 的 tool-loop 加 load_skill 拦截
- QC 检查维度改为先 load_skill 加载 project-directory-spec 再检查路径
This commit is contained in:
ymq 2026-08-21 18:36:18 +08:00
parent f17f745ac6
commit 71d30e6d45

View File

@ -346,6 +346,7 @@ async def _git_clone(repo_url, target_dir, branch='main'):
AGENT_TOOLS = [
{"name":"read_file","description":"读取工作空间中的文件","params":{"path":"相对路径"}},
{"name":"load_skill","description":"按需加载技能全文或子文件——需要具体规范/目录结构/路径/格式/流程时先加载对应技能(如 project-directory-spec 项目目录规范),不要凭记忆瞎写。只给 name 加载 SKILL.md 全文,给 file_path 加载 references/scripts/templates 下的子文件","params":{"name":"技能名","file_path":"子文件相对路径(可选,如 references/api.md)"}},
{"name":"write_file","description":"写入文件(自动创建父目录)","params":{"path":"相对路径","content":"文件内容"}},
{"name":"list_files","description":"列出目录内容","params":{"path":"相对路径(可选,默认工作空间根)"}},
{"name":"run_shell","description":"在工作空间中执行shell命令","params":{"command":"命令"}},
@ -372,6 +373,9 @@ __ROLE_SKILLS__
你可以使用以下工具完成工作
__TOOLS__
## 技能使用
上方可用技能是目录层只有名字+描述遇到需要具体规范目录结构产出路径文件格式流程约束的任务先用 load_skill 加载对应技能全文 project-directory-spec 项目目录规范不要凭记忆瞎写路径或格式
## 工作流
1. 先用 read_file/list_files 了解现有代码
2. write_file 产出代码文件到 repos/
@ -429,7 +433,12 @@ PM_SYSTEM_PROMPT = """你是项目经理PM。你的职责是项目计划
## 仓库状态
__REPO_STATE__
## 技能
__ROLE_SKILLS__
遇到需要具体规范目录结构路径格式流程的任务先用 load_skill 加载对应技能全文 project-directory-spec 项目目录规范不要凭记忆瞎写
## 工具
- load_skill(name, file_path?) 按需加载技能全文或 references/scripts/templates 子文件需要具体规范/路径/格式时用
- read_file(path) 读工作空间文件
- list_files(path) 列目录
- git_status() 查看git状态
@ -471,7 +480,7 @@ __REPO_STATE__
QC_SYSTEM_PROMPT = """你是质量控制工程师QC。对交付件做合规检查和质量检查不合规直接退回重做。
## 检查维度
1. 项目规范检查产出是否按 SDLC 仓库标准路径/命名/格式产出docs/ 目录结构apps/modules 描述文件文件命名规范
1. 项目规范检查产出是否按 SDLC 仓库标准路径/命名/格式产出先用 load_skill 加载 project-directory-spec 拿到权威目录结构与路径再据此检查交付文件都在 repos/ 不要凭记忆在工作空间根找 docs/apps/ 等目录
2. 项目过程规范是否遵循各阶段流程规范develop 是否实际产出代码文件test 是否覆盖充分deploy 配置是否完整requirement 是否明确部署环境需求
3. 产出质量内容是否完整可量化可验收无重大缺陷无空泛套话
@ -480,7 +489,12 @@ QC_SYSTEM_PROMPT = """你是质量控制工程师QC。对交付件做合
角色__ROLE__
工作目录__WORKSPACE__
## 技能
__ROLE_SKILLS__
遇到需要具体规范目录结构路径格式流程的任务先用 load_skill 加载对应技能全文不要凭记忆瞎写
## 工具
- load_skill(name, file_path?) 按需加载技能全文或 references/scripts/templates 子文件需要具体规范/路径/格式时用
- read_file(path) 读工作空间文件
- list_files(path) 列目录
- git_status() 查看git状态
@ -992,11 +1006,10 @@ async def _build_fallback_deliverable(workspace_dir, written_files, repos_dir, r
async def _build_role_skills_block(sor, project_id, role, org_id=""):
"""为角色 agent 构建技能注入块
"""为角色 agent 构建技能目录块(分层导入第一层)
角色专属技能scope=role pipelines/{pid}/roles/{role}/全量注入正文
角色 agent load_skill 工具且角色技能数量少是角色必须遵守的领域规范
其余 scope项目/产线/机构/通用注入目录层名字+描述按优先级降序排列
所有技能不分 scope统一注入目录层名字+描述按优先级降序排列
角色需要具体规范时用 load_skill 工具按需加载全文分层导入第二层
优先级同名覆盖角色 > 项目 > 产线 > 机构 > 通用SCOPE_PRIORITY 已定义
返回注入 system prompt 的文本无技能或失败时返回空串
"""
@ -1012,24 +1025,52 @@ async def _build_role_skills_block(sor, project_id, role, org_id=""):
project_id=project_id, org_id=org_id or '0')
if not merged:
return ""
blocks = []
role_skills = [s for s in merged.values() if getattr(s, 'scope', '') == 'role']
for s in role_skills:
blocks.append(s.to_prompt_block())
others = sorted([s for s in merged.values() if getattr(s, 'scope', '') != 'role'],
skills = sorted(merged.values(),
key=lambda s: -SCOPE_PRIORITY.get(getattr(s, 'scope', ''), 0))
if others:
lines = ["## 可用技能(目录,优先级 角色>项目>产线>组织>通用)"]
for s in others[:60]:
tag = SCOPE_TAG.get(getattr(s, 'scope', ''), getattr(s, 'scope', ''))
lines.append(f"- [{tag}] {s.name}: {(s.description or '')[:100]}")
blocks.append("\n".join(lines))
return "\n\n".join(blocks)
lines = ["## 可用技能(目录,按需用 load_skill 加载全文;优先级 角色>项目>产线>组织>通用)"]
for s in skills[:60]:
tag = SCOPE_TAG.get(getattr(s, 'scope', ''), getattr(s, 'scope', ''))
lines.append(f"- [{tag}] {s.name}: {(s.description or '')[:100]}")
return "\n".join(lines)
except Exception as e:
logger.warning(f"role skills load failed: {e}")
return ""
async def _load_skill_by_name(sor, project_id, role, org_id, name, file_path=None):
"""按需加载技能全文或子文件(分层导入第二层,对应 load_skill 工具)。
- 不带 file_path返回技能 SKILL.md 全文 + 关联文件清单references/scripts/templates/assets
- file_path返回 skill 目录下对应子文件内容仅限 references/scripts/templates/assets
"""
name = (name or '').strip()
if not name:
return 'FAIL: 需要技能名称'
try:
pid = await _resolve_pipeline_id(project_id)
from pipeline_core.skill_loader import get_skill_loader
from pipeline_core.skill_pack import ensure_org_skills
from .workspace import get_workspace_base
ws_base = await get_workspace_base(sor)
skills_dir = ensure_org_skills(ws_base, org_id or '0')
loader = get_skill_loader(skills_dir)
merged = loader.get_merged(pipeline_id=pid, role=role,
project_id=project_id, org_id=org_id or '0')
if name not in merged:
names = ", ".join(sorted(merged.keys())) or "(无可用技能)"
return f"FAIL: 技能 '{name}' 不存在。可用技能: {names}"
skill = merged[name]
if file_path:
return skill.read_linked_file(file_path)
body = skill.to_prompt_block()
linked = skill.list_linked_files()
if linked:
body += "\n\n## 关联文件(可用 load_skill(name, file_path) 按需加载)\n" + "\n".join(f"- {f}" for f in linked)
return body
except Exception as e:
return f'ERROR: {str(e)[:300]}'
async def role_agent_run(project_id, role, agent_id=None, model_name=None):
role, _role_specific, _ = await _resolve_role(project_id, role)
if role == 'pm':
@ -1137,7 +1178,10 @@ async def role_agent_run(project_id, role, agent_id=None, model_name=None):
ask_question = params.get("question", "")
break
result = await _exec_agent_tool(tool, params, workspace_dir)
if tool == "load_skill":
result = await _load_skill_by_name(sor, project_id, role, org_id, params.get("name", ""), params.get("file_path") or None)
else:
result = await _exec_agent_tool(tool, params, workspace_dir)
if tool == "write_file" and params.get("path"):
written_files.append(os.path.join(workspace_dir, params["path"]))
msgs.append({"role": "tool", "tool_call_id": tc.get("id", ""), "content": str(result)})
@ -1158,7 +1202,10 @@ async def role_agent_run(project_id, role, agent_id=None, model_name=None):
elif act.get('action') == 'tool_call':
tool = act.get('tool', '')
params = act.get('params', {})
result = await _exec_agent_tool(tool, params, workspace_dir)
if tool == 'load_skill':
result = await _load_skill_by_name(sor, project_id, role, org_id, params.get('name', ''), params.get('file_path') or None)
else:
result = await _exec_agent_tool(tool, params, workspace_dir)
if tool == 'write_file' and params.get('path'):
written_files.append(os.path.join(workspace_dir, params['path']))
msgs.append({"role": "assistant", "content": raw})
@ -1447,12 +1494,14 @@ async def pm_review_run(project_id, agent_id=None, model_name=None):
repo_state = await _get_repo_state(workspace_dir)
repos_str = ", ".join([r['name'] for r in repos]) if repos else ""
content_preview = deliverable_content[:6000]
role_skills = await _build_role_skills_block(sor, project_id, 'pm', org_id)
pm_system = PM_SYSTEM_PROMPT.replace('__TITLE__', title)\
.replace('__ROLE__', task_role)\
.replace('__WORKSPACE__', workspace_dir)\
.replace('__REPOS__', repos_str)\
.replace('__REPO_STATE__', repo_state)
.replace('__REPO_STATE__', repo_state)\
.replace('__ROLE_SKILLS__', role_skills)
msgs = [{"role": "system", "content": pm_system}]
msgs.append({"role": "user", "content": f"请审核以下交付件(类型:{deliverable_type}\n\n{content_preview}"})
@ -1512,6 +1561,8 @@ async def pm_review_run(project_id, agent_id=None, model_name=None):
result = await _pm_list_tasks(sor, project_id, params)
elif tool in ('cancel_task', 'cancel'):
result = await _pm_cancel_task(sor, project_id, params)
elif tool == 'load_skill':
result = await _load_skill_by_name(sor, project_id, 'pm', org_id, params.get('name', ''), params.get('file_path') or None)
else:
if turn >= 3:
# 硬约束最后两轮拒绝执行探索类工具read_file/git_status/list_files/run_shell 等),
@ -1678,10 +1729,12 @@ async def qc_review_run(project_id, agent_id=None, model_name=None):
return {"status": "rejected", "task_id": task_id, "reason": "没有交付件"}
content_preview = deliverable_content[:6000]
role_skills = await _build_role_skills_block(sor, project_id, 'qc', org_id)
qc_system = QC_SYSTEM_PROMPT.replace('__TITLE__', title)\
.replace('__ROLE__', task_role)\
.replace('__WORKSPACE__', workspace_dir)
.replace('__WORKSPACE__', workspace_dir)\
.replace('__ROLE_SKILLS__', role_skills)
msgs = [{"role": "system", "content": qc_system}]
msgs.append({"role": "user", "content": f"请检查以下交付件(类型:{deliverable_type}\n\n{content_preview}"})
@ -1716,7 +1769,10 @@ async def qc_review_run(project_id, agent_id=None, model_name=None):
elif act.get('action') == 'tool_call':
tool = act.get('tool', '')
params = act.get('params', {})
result = await _exec_agent_tool(tool, params, workspace_dir)
if tool == 'load_skill':
result = await _load_skill_by_name(sor, project_id, 'qc', org_id, params.get('name', ''), params.get('file_path') or None)
else:
result = await _exec_agent_tool(tool, params, workspace_dir)
msgs.append({"role": "assistant", "content": raw})
msgs.append({"role": "user", "content": f"工具 {tool} 结果:\n{result}"})
else: