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:
parent
f17f745ac6
commit
71d30e6d45
@ -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:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user