From 71d30e6d451ad75d0efa3edb1ce3d0585cdb5cca Mon Sep 17 00:00:00 2001 From: ymq Date: Fri, 21 Aug 2026 18:36:18 +0800 Subject: [PATCH] =?UTF-8?q?feat(sdlc):=20=E6=8A=80=E8=83=BD=E5=88=86?= =?UTF-8?q?=E5=B1=82=E5=AF=BC=E5=85=A5=E7=BB=9F=E4=B8=80=E2=80=94=E2=80=94?= =?UTF-8?q?=E6=89=80=E6=9C=89=E8=A7=92=E8=89=B2=E6=8A=80=E8=83=BD=E5=8F=AA?= =?UTF-8?q?=E6=B3=A8=E7=9B=AE=E5=BD=95=E5=B1=82=20+=20load=5Fskill=20?= =?UTF-8?q?=E6=8C=89=E9=9C=80=E5=8A=A0=E8=BD=BD=E5=85=A8=E6=96=87/?= =?UTF-8?q?=E5=AD=90=E6=96=87=E4=BB=B6(=E5=90=AB=20PM/QC)=EF=BC=8C?= =?UTF-8?q?=E5=AF=B9=E9=BD=90=20Hermes=20=E5=88=86=E5=B1=82=E6=9C=BA?= =?UTF-8?q?=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - _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 再检查路径 --- pipeline_service/agent_loop.py | 100 +++++++++++++++++++++++++-------- 1 file changed, 78 insertions(+), 22 deletions(-) diff --git a/pipeline_service/agent_loop.py b/pipeline_service/agent_loop.py index 333fc07..7310bd0 100644 --- a/pipeline_service/agent_loop.py +++ b/pipeline_service/agent_loop.py @@ -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: