14 KiB
| name | description |
|---|---|
| cockpit-agent-patterns | Cockpit session agent anti-patterns and fixes. |
Cockpit Agent 设计模式
会话 agent(cockpit_chat.dspy)开发中发现的陷阱和修复。
对话历史结构化
反模式: 历史以独立 user/assistant 角色混入 LLM 上下文,LLM 模仿旧 tool_call。
正确做法: 聚合为系统级"参考历史记录:",用户当前输入为唯一 user 消息:
[system] 参考历史记录:
用户: 切换到人事系统
助手: OK 已切换
[user] 人事系统项目当前进展 ← 唯一主输入,历史不会污染决策
项目名匹配
反模式: _find_project 硬编码精确/子串匹配。
正确做法: 精确匹配失败时调 _call_llm_raw(temp=0) 做文本分类——给 LLM 用户输入和项目列表,让它选。不要硬编码匹配逻辑。
owner 级配置 get/set 不匹配(保存后看不到)
反模式: 工作环境等 owner 级配置(user/org)用 get_work_env(sor, user_id, org_id) 读取,优先级 user → org。专门的 org 级配置页面(如 sd_org_remote「机构远程空间」)加载时走默认 get,返回的是 user 级配置(往往是空/local),用户保存到 org 级后退出再进,get 又返回 user 级 → 看起来"保存没生效"。
修复: get API 加 owner_type 参数。org 页面加载传 owner_type=org(get_work_env(sor, '', org_id) 只查 org 级),user 页面传 owner_type=user(只查 user 级)。前端加载 script 用 ?action=get&owner_type=org,与 set 的 owner_type='org' 对齐。
切换项目不持久化(下一轮回退老项目)
反模式: _t_switch_project/_t_create_project 只改 self.project_id(AgentExecutor 实例内存字段),不写 pipeline_agent_settings。AgentExecutor 每轮 run 新建、run 结束销毁,所以切换在下一条消息就回退。
修复: switch/create 成功后必须持久化 current_project_id 到 pipeline_agent_settings(user_id 唯一键,UPDATE-first + SELECT-check + INSERT 兜底)。否则 cockpit_chat_v2 每轮从 settings 读旧项目,历史隔离也跟着错(切到新项目却加载旧项目历史)。
需求描述被当"查进展"(新需求没被关注)
反模式: system_prompt 写"用工具查询数据" + 示例 diagnose_project,把 LLM 往"诊断/查询"带;auto-inject 兜底又按关键词默认注入 diagnose_project/list_tasks。用户发需求描述(含"系统/模块/任务"等词)时,agent 去 diagnose 旧任务而不是 create_task 推进需求。
修复(三层):
- system_prompt 明确工作流:"描述需求→create_task+start_agents;问进展→list_tasks/diagnose;问问题→list_questions"。
- auto-inject 兜底加"新需求"启发式:
len(input)>40 且含需求特征词(实现/开发/系统/功能/模块/切换/监控/备份/同步...)→ 注入 create_task 引导,而非 diagnose。 - 需求特征词表放代码里,prompt 保持简洁(不靠冗长禁令)。
create_project 不设置 workspace_dir(所有项目共用 default 目录)
反模式: _t_create_project 只 sor.C("sd_projects", {id,name,description,status}),不设 workspace_dir/org_id。结果 _resolve_workspace(agent_loop.py)遇到空 workspace_dir 回退 ~/pipeline_ws/default,所有项目共用同一目录,设计文档/代码互相覆盖;而前端 get_workspace_path(workspace.py)回退 /d/pipeline/workspaces/<org>/<项目名>,两处不一致 → 前端工作空间浏览器显示"不可用",文档实际写在 default。
修复: create_project 时设 workspace_dir = os.path.join(workspace_base, org_id, name) + os.makedirs,org_id 从 users.orgid 查。workspace_base 不硬编码,从 appbase params 表读(params_name='workspace_base' → params_value),带默认值 /d/pipeline/workspaces 兜底(_get_param try/except,params 表不存在时用默认值不 500)。现有项目要手动 UPDATE sd_projects SET workspace_dir=... 并把 default 里的文档 mv 回项目目录。
tool_call JSON 泄露
反模式: LLM 在 reply 中夹杂 {"action":"tool_call",...}。
修复: _parse() 用 regex 剥离 tool_call JSON,rstrip('}') 清除残留花括号。
LLM 光说不练(deepseek 特有问题)
反模式: "我来逐一回答"但不调 answer_question,"启动Agent"但不调 start_agents。
更严重的是 deepseek-v4-pro:即使 prompt 明确要求"第一步必须调用工具"、"禁止 reply",LLM 仍然
第一个动作就输出 {"action":"reply",...}。尝试了激进规则、few-shot 示例、移除 reply 格式选项、
精简到 3 行核心指令——全部无效。
修复(双阶段代码级兜底):
-
首轮注入(
_tool_call_count == 0时):- 根据用户输入关键词推断合适工具(任务→list_tasks,问题→list_questions,其他→diagnose_project)
- 拦截 reply,追加 "请调用 {tool},只输出JSON" 到消息数组,continue 继续 loop
-
延续推动(
_tool_call_count <= 2时):- LLM 调了 1-2 次工具就想停 → 追加 "请继续:查看失败任务详情、回答问题、或启动agent"
- 防止 LLM 拿到诊断结果就 reply 而非继续深入
关键:_tool_call_count 只在工具成功返回时递增(结果不以 "未知工具" 或 "ERROR" 开头)。
换用 Claude/GPT-4 后此机制可移除——prompt 本身就能驱动工具调用。
详见: references/deepseek-tool-calling.md
⚠️ 2026-08-15 已废弃:上面的 auto-inject 双阶段兜底(硬编码 req_keywords + 强制注入 hint_tool)已被移除。它把 agent 变成了"只能路由工具的傻子"——见下一节。诚实降级取代强制路由。
会话 agent 不是"路由器"——诚实降级 + 能力自省(2026-08-15 重构)
反模式(三重病态叠加,把 agent 变成傻子路由器):
- system prompt 写
必须只输出 tool_call JSON,禁止 reply→ 剥夺 LLM 说"我做不到"的能力,面对超出工具集能力的指令只能硬套一个最接近的工具。 - auto-inject 用硬编码关键词(
req_keywords=["实现","开发","系统","模块"...]+if kw in user_input)强制注入工具 → 违反"意图识别交给 LLM 不硬编码"铁律,LLM 想停就被塞 hint_tool。 ask_user是假的:return f"QUESTION: {params['question']}"把问题回传给 LLM 自己,用户看不到 → LLM 连"问用户"都做不到。
触发症状:用户指令超出工具集能力(如"重做三个模块"但无 reset_task 工具)时,agent 不诚实说明,而是退化成 diagnose→list_tasks→list_deliverables→list_questions→run_command 找规范文件,最后卡在 run_command 的 confirm 弹窗(requires_confirmation)没人确认就搁置。
修复(把 Hermes 通用能力还给它):
- 删"禁止 reply"。prompt 写"能力自省与诚实降级:请求超出工具能力时,明确告诉用户你做不到+缺什么能力+给替代方案+必要时 ask_user 拍板"。
- 删 auto-inject 硬编码关键词。
reply和ask_user是合法终止动作(直接输出并 return,不再强制注入工具)。 ask_user真正抛给用户:作为终止动作yield {"type":"ask_user","message":q}给前端,前端 cockpit_chat_v2.dspy 加elif t == 'ask_user'分支展示❓ 问题。
用户定位(架构原则):会话 agent = Hermes 通用能力(推理/判断/诚实降级/能力自省/澄清)+ 项目工具,不是"只能路由、不能判断的壳"。auto-inject 只在"LLM 真·空转(连续 N 轮既无 tool_call 也无 reply/ask_user)"时才温和提示一次,且不硬编码关键词。
Prompt 占位符未替换
反模式: system prompt 模板中有 {tools_description} 占位符,但 _build_system_prompt
将其作为字面文本传给 LLM——LLM 看到的是 ## 可用工具\n{tools_description} 而非实际工具列表。
修复: _build_system_prompt 中必须构建 tools_text 后用 prompt.replace("{tools_description}", tools_text) 替换。
同时 {current_project} 应替换为项目名而非项目 ID(_load_project_context 获取名称)。
msgs 数组污染
反模式: msgs.append({"role":"assistant","content":raw}) 把原始 tool_call JSON 塞进 LLM 上下文。
修复: 改为 msgs.append({"role":"assistant","content":"已调用 {tool}"})
通知推送
反模式: 角色 agent 提问后会话 agent 不主动告知用户。
修复: 每轮开始查询 pending questions,yield 为第一条 NDJSON + 注入 system prompt。
回复控件
反模式: 最终回复用 Text 控件,不支持 markdown。
修复: 最终回复/通知用 MdWidget(支持 粗体、列表、换行),工具结果用 Text。
_save_ctx 竞态
反模式: SELECT-then-INSERT 导致 duplicate key。
修复: UPDATE-first + try/except,INSERT 仅兜底。
登录门禁
反模式: get_user() 返回 None 时静默失败。
修复: send_message/list_messages 入口检查,None 返回"请先登录"。
claimed_by 僵尸锁(PM 驳回后任务永久卡死)
反模式: PM agent 驳回交付件时只 UPDATE state='submitted',不清 claimed_by。
agent poller 条件 claimed_by IS NULL → 跳过此任务 → 任务永远不被认领。
修复: PM reject 路径必须 SET state='submitted', claimed_by=NULL。
同时定期清理:UPDATE pipeline_tasks SET claimed_by=NULL WHERE state='submitted' AND claimed_by IS NOT NULL。
进程重启不生效(start.sh kill -0 陷阱)
症状: 代码已部署到 site-packages,重启后旧行为依旧。
根因: start.sh 用 kill -0 $(cat pipeline.pid) 检查旧进程。PID 文件被删但进程还活着时,
kill -0 返回成功 → 打印 "Already running" → 跳过启动 → 旧进程代码永不被替换。
修复: 部署最后一步必须 pkill -9 -f pipeline_app 强制杀进程,再 bash start.sh。
进阶陷阱(多进程并存 + pkill 255 断链)——重启流程两个独立坑,都会造成"改完还报旧错":
-
pkill -9 -f pipeline_app在没匹配到进程时返回 255,放在&&命令链里会断掉后续的bash start.sh。所以 kill 和 start 必须分两条命令执行,或用;而非&&,或用ps aux|grep|awk '{print $2}'|xargs -r kill -9(-r空输入不报错)。 -
反复重启若没彻底杀,会累积多个 pipeline_app 进程,最早的进程占着 9090 端口,新启动的进程监听失败但 start.sh 仍打印 "Started",curl 打到旧进程 → 修复看起来"没生效"。改完代码必须验证:
ps aux | grep pipeline_app | grep -v grep | wc -l # 必须 = 1
部署后先确认单进程、再 curl 验证,否则排查方向会被旧进程带偏(本次 remote_dir 相对路径验证连踩 3 次"假失败")。
LLM 编造工具名
反模式: LLM 输出 tapd_get_tasks、run_shell_command、get_task_detail 等不存在的工具名。
修复: 在 _dispatch_sdlc_tool 的 handlers dict 中加别名映射:
get_tasks/get_task_list→list_tasksget_task/get_task_detail→task_detailget_deliverable→view_deliverable
list_tasks 输出格式化
反模式: "- [state] title (角色: role) id=xxx" 纯文本一坨,MdWidget 渲染效果差。
修复: 输出 markdown 表格 + emoji 图标:
| 状态 | 任务 | 角色 | ID |
|------|------|------|----|
| ⏳ submitted | 开发模块 | design | abc123 |
| ✅ approved | ... | ... | ... |
用户意图识别(停止 / 补充 / 新任务)
反模式: 新消息到达时前端自动 abort 旧请求。
正确做法:
- 后端检测停止关键词(停止/取消/停/stop/cancel)→ 立即返回"已停止"
- 其他消息照常处理为新对话轮次
- 不前断 abort——用户可能在补充信息
工具结果 MdWidget 渲染
反模式: list_tasks 返回 markdown 表格,但 cockpit_chat_v2.dspy 的 _w_card 用 _w_text 包裹——表格显示为原始 |...| 文本。
修复: cockpit_chat_v2.dspy 的 tool_result handler 检测结果是否以 |, #, - 开头(markdown 特征),是则用 _w_md(result[:2000]) 作为 card body:
is_md = result.startswith('|') or result.startswith('#') or result.startswith('- ')
body = _w_md(result[:2000]) if is_md else result[:600]
Pipeline 浏览器登录
CDP 浏览器测试 pipeline cockpit 的登录流程:
https://pipeline.opencomputing.cn/rbac/user/login.ui— Bricks 登录表单- 填入 username/password(如 admin/admin123),点击 Form Submit 按钮
- 或用 fetch POST
/rbac/user/up_login.dspy(注意必须有/user/前缀)+_webbricks_=1 password_encode()在ahserver/globalEnv.py,用 RC4 + 配置 key;DB 存的是 encoded 值- 登录后 session 生效,导航到
/pipeline-sdlc即可渲染 cockpit
验证前端的正确姿势(SSRF 阻止私有地址):browser_navigate 会拒绝 localhost/127.0.0.1("Blocked: URL targets a private address",config browser.allow_private_urls:false),所以 SSH 隧道 localhost:19090 对浏览器工具无效——别在隧道上浪费时间。直接 browser_navigate 到生产公网地址 https://pipeline.opencomputing.cn/...(公网不触发 SSRF 防护)。先 browser_cdp 调 Target.getTargets 确认 CDP 可达(config browser.cdp_url: localhost:9222),再导航。若页面 body 显示的是 .ui 的 JSON 源码而非渲染表单(select/input 数量为 0、bricks.app 未 run),是未登录/渲染失败,用 browser_console 查 js_errors 定位,不是修复引入的问题。