From c848dc1d4ce1751252e6950fe628c7c222135c4c Mon Sep 17 00:00:00 2001 From: ymq Date: Thu, 20 Aug 2026 14:37:14 +0800 Subject: [PATCH] =?UTF-8?q?refactor(agent=5Floop):=20=E5=88=A0=E9=99=A4=20?= =?UTF-8?q?v1=20=E7=A1=AC=E7=BC=96=E7=A0=81=E8=A7=92=E8=89=B2=E5=AE=9A?= =?UTF-8?q?=E4=B9=89=EF=BC=8C=E7=BB=9F=E4=B8=80=E8=B5=B0=E8=83=BD=E5=8A=9B?= =?UTF-8?q?=E5=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 删 ROLE_SPECIFICS(角色 prompt 硬编码,已被 sdlc_ability.SDL_ROLES 完整覆盖) - 删 ROLE_CHAIN(任务链硬编码,已被 RoleSpec.next_role 覆盖) - _resolve_role/_get_next_role 去掉 fallback,只走 get_role_spec - 删死代码 PM_PROMPT/_is_safe_workdir/_parse_result(0 引用) - 保留 ROLE_ALIASES:服务 _normalize_role 的 who 规范化(27 处调用点依赖), 与能力包角色 prompt 是不同职责 --- pipeline_service/agent_loop.py | 151 +++++---------------------------- 1 file changed, 22 insertions(+), 129 deletions(-) diff --git a/pipeline_service/agent_loop.py b/pipeline_service/agent_loop.py index 6f849f9..dd1d696 100644 --- a/pipeline_service/agent_loop.py +++ b/pipeline_service/agent_loop.py @@ -36,15 +36,6 @@ ROLE_ALIASES = { 'pm': 'agent.pm', 'project_manager': 'agent.pm', '项目经理': 'agent.pm', 'manager': 'agent.pm', } -ROLE_CHAIN = { - 'agent.requirement': 'agent.design', - 'agent.design': 'agent.develop', - 'agent.develop': 'agent.deploy_test', - 'agent.deploy_test': 'agent.test', - 'agent.test': 'agent.deploy_prod', - 'agent.deploy_prod': None, -} - TASK_REVIEW = 'review' TASK_APPROVED = 'approved' @@ -91,10 +82,7 @@ async def _resolve_pipeline_id(project_id): async def _resolve_role(project_id, role): - """从能力包解析角色定义,返回 (normalized_role, role_specific_prompt, next_role)。 - - 能力包未定义该角色时 fallback 到硬编码 ROLE_SPECIFICS/ROLE_CHAIN。 - """ + """从能力包解析角色定义,返回 (normalized_role, role_specific_prompt, next_role)。""" pid = await _resolve_pipeline_id(project_id) try: from pipeline_core import get_role_spec @@ -104,7 +92,7 @@ async def _resolve_role(project_id, role): except Exception: pass norm = _normalize_role(role) - return norm, ROLE_SPECIFICS.get(norm, ROLE_SPECIFICS.get('develop', '')), ROLE_CHAIN.get(norm) + return norm, "", "" async def _resolve_llm_context(sor, project_id, role, model_name=None): @@ -181,16 +169,6 @@ def _resolve_workspace(workspace_dir): # ── Agent 工具函数 ── -def _is_safe_workdir(workdir): - """检查目录是否在允许范围内(同步版,仅用模块级白名单兜底)。""" - wd = os.path.abspath(workdir) - for allowed in _ALLOWED_WORKDIRS: - awd = os.path.abspath(os.path.expanduser(allowed)) - if wd.startswith(awd): - return True - return False - - _allowed_workdirs_cache = None _allowed_workdirs_cache_time = 0 @@ -408,94 +386,6 @@ __TOOLS__ 注意:每次只输出一个JSON!收到工具结果后再决定下一步。""" -ROLE_SPECIFICS = { - 'requirement': """你是需求分析师。按 SDLC 仓库标准产出文档。 - -产出路径(项目过程仓库 repos/project/ 下): - - docs/00-requirement/requirement-spec.md - - apps/<应用名>.md(应用 = 部署单元,至少一个,含端口/环境) -内容: 项目概述、用户角色及权限、功能列表(每个功能:输入/处理/输出/验收标准)、非功能需求、业务流程。 - -注意: 需求阶段只识别「应用」(部署单元),**不划分模块**——模块划分是架构决策,由 design 阶段的设计师完成。 -完成后用 git_commit_push 提交到项目过程仓库 repos/project/,再用 result 输出文档、files 列出文件路径。""", - - 'design': """你是系统设计师。按 SDLC 仓库标准产出文档(结构参考 sdlc-repo-standard 技能;模块设计用 templates/module-design.md 模板,模块技能用 templates/module-skill.md 模板,保证 develop 读到一致结构)。 - -应用级设计(产出到项目过程仓库 repos/project/docs/01-design/): - - architecture.md: 系统架构、技术选型理由、模块划分、模块间依赖关系、开发顺序 - - ui-design.md: 视觉风格(配色/字体/间距)、主页面布局、用户交互模式、弹窗/表单规范 - -模块级设计(每个模块自包含、可复用,产出到 repos/project/modules/): - - modules/<模块名>.md: 模块功能、仓库URL(先填规划地址)、技术栈、依赖、开发顺序 - - modules/<模块名>/design.md: 该模块的数据设计(表结构DDL) + CRUD 定义 + 处理逻辑(接口) - - modules/<模块名>/skill/SKILL.md: 该模块技能文档 - 数据、CRUD、处理逻辑、skills 都属于模块本身,不放在应用级 design——这样模块可整体复用。 - -模块划分遵循: 概念相近的功能组合成一个独立模块,每个模块独立仓库。apppublic、sqlor、ahserver、accounting、appbase、rbac 等基础模块已存在、可直接引用,**不必列为待开发模块**。后续 PM 按你划分好的模块 + 依赖关系派发 develop 任务。 -完成后用 git_commit_push 提交到项目过程仓库 repos/project/,再用 result 输出文档、files 列出文件路径。""", - - 'develop': """你是开发工程师。按模块化原则开发:概念相近的功能组合成一个独立模块,每个模块独立仓库。源码写入模块独立仓库 repos/<模块名>/,不在项目过程仓库。 - -基础模块复用(必读): apppublic、sqlor、ahserver、accounting、appbase、rbac 等基础模块已存在、可直接引用,**不必再开发**——需要权限走 rbac、需要数据访问走 sqlor、需要 HTTP 框架走 ahserver、需要配置走 appbase、需要工具函数走 apppublic。 - -准备工作: - 1. read_file 读 repos/project/docs/01-design/ 下架构与 UI 设计文档 - 2. read_file 读 repos/project/modules/<模块名>.md 获取模块仓库 URL,读 repos/project/modules/<模块名>/design.md 获取数据设计/CRUD/处理逻辑 - 3. git_clone 克隆模块仓库到 repos/ 下 -开发流程: - 4. write_file 写代码到模块仓库目录 - 5. run_shell 编译/运行验证 - 6. git_commit_push 提交模块仓库 "develop: <简述>" - 7. write_file 更新 repos/project/modules/<模块名>.md 状态为已完成 - 8. write_file 写 repos/project/docs/02-develop/dev-notes.md 记录开发内容,并 git_commit_push 提交项目过程仓库 - 9. deliver 交付,files 列出所有产出文件路径 -PM审核: git_status 检查模块仓库有提交记录。""", - - 'test': """你是测试工程师。按 SDLC 仓库标准产出文档。 - -产出路径(项目过程仓库 repos/project/docs/03-test/): - - test-plan.md: 测试策略(单元/集成/端到端) - - test-cases.md: 用例清单(编号/前置条件/步骤/预期结果) - - test-report.md: 执行结果、Bug清单、覆盖率 -完成后用 git_commit_push 提交到项目过程仓库 repos/project/。""", - - 'deploy': """你是部署运维工程师。按 SDLC 仓库标准产出文档和配置。 - -产出路径(项目过程仓库 repos/project/ 下): - - docs/04-deploy/deploy-guide.md: 环境要求、部署步骤、回滚方案 - - docs/04-deploy/release-notes.md: 发布说明 - - config/ 下: Dockerfile、nginx配置、docker-compose.yml 等 -完成后用 git_commit_push 提交到项目过程仓库 repos/project/,再用 result 输出部署说明、files 列出配置文件路径。""", -} - -PM_PROMPT = """你是项目经理(PM)。审核交付件并推动项目前进。 - -## 项目 -工作目录:__WORKSPACE__ -关联仓库:__REPOS__ - -## 待审核任务 -标题:__TITLE__ -角色:__ROLE__ -交付件摘要:__SUMMARY__ -交付件内容(前6000字):__DELIVERABLE__ - -## 仓库状态 -__REPO_STATE__ - -## 决策 -输出纯JSON: -1. 通过:{"status":"approved","comment":"审核意见","next_task_title":"下阶段标题","next_task_description":"详细描述"} -2. 驳回:{"status":"rejected","comment":"驳回原因","questions":"修改要求"} -3. 完成:{"status":"completed","comment":"项目总结"} - -审核标准: -- requirement:需求是否清晰完整可量化 -- design:方案合理、覆盖需求、技术可行 -- develop:代码文件是否实际产出、目录结构是否合理、是否可编译运行 -- test:测试覆盖充分、发现问题记录完整 -- deploy:部署配置完整、可一键部署""" - PM_SYSTEM_PROMPT = """你是项目经理(PM)。你的职责是项目计划、任务分配、任务验收。 ## 项目推进铁律(最高优先级) @@ -700,19 +590,6 @@ async def _build_qna_section(sor, task_id, role, agent_id=None): return "\n".join(lines) -def _parse_result(raw): - raw = (raw or "").strip() - if raw.startswith("```"): - raw = raw.split("\n", 1)[1].rsplit("```", 1)[0] - try: - d = json.loads(raw) - if isinstance(d, dict): - return d - except (json.JSONDecodeError, ValueError): - pass - return {"status": "done", "result": raw} - - async def _get_deliverable_content(sor, task_id): recs = await sor.sqlExe( "SELECT content, deliverable_type FROM pipeline_deliverables " @@ -791,7 +668,7 @@ async def _get_repo_state(workspace_dir): async def _get_next_role(current_role, project_id=""): - """任务链下一角色:优先从能力包取,fallback 硬编码 ROLE_CHAIN。""" + """任务链下一角色:从能力包取。""" if project_id: try: pid = await _resolve_pipeline_id(project_id) @@ -801,7 +678,7 @@ async def _get_next_role(current_role, project_id=""): return spec.next_role # 可能是 ""(终结) except Exception: pass - return ROLE_CHAIN.get(_normalize_role(current_role)) + return "" async def _create_next_task(sor, project_id, task, next_role, pm_comment=''): @@ -1615,7 +1492,13 @@ async def pm_review_run(project_id, agent_id=None, model_name=None): elif tool in ('cancel_task', 'cancel'): result = await _pm_cancel_task(sor, project_id, params) else: - result = await _exec_agent_tool(tool, params, workspace_dir) + if turn >= 3: + # 硬约束:最后两轮拒绝执行探索类工具(read_file/git_status/list_files/run_shell 等), + # 强制 PM 立即输出决策——否则 deepseek 无视软提示持续检查、5 轮耗尽 → 审核超时。 + result = (f"已到最后收尾阶段(第 {turn + 1}/5 轮),拒绝执行探索类工具 {tool}。" + f"请立即输出 review_approve / review_reject / review_complete / review_rollback 之一,不要再调用工具。") + 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: @@ -1630,7 +1513,17 @@ async def pm_review_run(project_id, agent_id=None, model_name=None): continue if not decision: - decision = {'status': 'rejected', 'comment': '审核超时'} + # 超时智能处理:PM 超时前可能已 create_tasks 派发后续子任务(parent_id 指向本任务), + # 说明 PM 实质已认可本任务(approve 行为)。此时默认 approved 而非 rejected—— + # 否则 design 被无意义的「审核超时」驳回 → 重做写占位文件 → QC 死循环(2026-08 实测)。 + child_recs = await sor.sqlExe( + "SELECT COUNT(*) as c FROM pipeline_tasks WHERE parent_id=${tid}$", {"tid": task_id}) + await sor.sqlExe("COMMIT", {}) + child_cnt = getattr(child_recs[0], 'c', 0) if child_recs else 0 + if child_cnt > 0: + decision = {'status': 'approved', 'comment': '审核超时,但 PM 已派发后续任务(create_tasks),视为认可通过'} + else: + decision = {'status': 'rejected', 'comment': '审核超时'} status = decision['status'] comment = decision.get('comment', '')