8.0 KiB
Raw Blame History

name description
cockpit-agent-dev Cockpit agent v1 DSPY + v2 AgentExecutor patterns, pitfalls, tool-loop fixes.

Cockpit Agent 开发规范

架构

  • v1: cockpit_chat.dspy — 会话 agentLLM tool-loop 架构(已删除,连同旧界面 sd_cockpit/index.ui
  • v2: cockpit_chat_v2.dspy — AgentExecutor 驱动pipeline-core/service v2 架构(当前唯一,前端 index.ui 的 AgentIO url 指向它)

只保留 v2。改 cockpit 逻辑只改 cockpit_chat_v2.dspy + AgentExecutor不要再去改已删除的 v1cockpit_chat.dspy。历史遗留过两个入口index.uiAgentIO→v2sd_cockpit/index.uiTextFiles→v1后者已删。

  • NDJSON 流式响应:每行 {"widgettype":"...","options":{...}}\n
  • 前端 AgentIO widget 通过 HttpResponseStream.handle_chunk 逐行解析

tool-loop 关键模式

1. LLM 消息数组不污染

# ❌ 错误LLM 看到自己输出的 tool_call JSON下轮模仿
msgs.append({"role":"assistant","content":raw})

# ✅ 正确:用清理后的文本
msgs.append({"role":"assistant","content":f"已调用 {tool}"})

2. 对话历史过滤

加载历史时跳过 {"action":"tool_call" 开头的消息,防止旧 tool_call 污染新对话。

3. 项目上下文持久化

  • _save_ctx / _load_ctx 使用 pipeline_agent_settings
  • iteration_id 字段借用于存储 project_idSDLC conversation 表中无 project_id
  • get_user() 返回 None 时跳过所有 DB 持久化,仅内存中维持

4. LLM 文本分类模式

当精确匹配失败时,用独立 LLM 调用做分类,不污染主 agent 上下文:

async def _call_llm_raw(prompt, temp=0.0):
    # 独立获取 model独立 API 调用30s 超时

5. reply 文本清理

_parse 中用正则剥离 {"action":"tool_call",...} JSON 片段后再返回给前端。

Widget 系统

def _w_text(t): return {"widgettype":"Text","options":{"text":t,"css":"agent-text","halign":"left"}}
def _w_card(title, body, kind): ...  # VBox with border-left color
def _w_progress(text): ...           # orange progress text
def _w_md(t): ...                    # MdWidget for markdown replies

v2 AgentExecutor 集成

cockpit 已支持 v2 执行引擎。端点:cockpit_chat_v2.dspy

切换方式:改 index.ui 中 AgentIO 的 url 指向 cockpit_chat_v2.dspy

{"url": "/pipeline-sdlc/api/cockpit_chat_v2.dspy"}

v2 使用 pipeline-core.agent_config.AgentConfig + pipeline-service.agent_loop_v2.AgentExecutor

deepseek-v4-pro 工具调用问题

该模型不遵循 system prompt 中的「必须先调工具」指令,会直接 reply。 解决方案:代码级 auto-inject。 详见 pipeline-agent-architecture 技能 references/v2-auto-inject.md

关键要点:

  • 工具别名表LLM 会编造 get_tasksget_task_detail 等名称
  • 最小化 prompt删除冗长规则工具列表放前面
  • auto-inject 限制:最多推 3 次(_auto_push_count),防止死循环
  • 失败调用不计数:未知工具ERROR 结果不增加 _tool_call_count

用户意图识别(停止 / 补充 / 新任务)

不要粗暴 abort 旧请求。 用户新消息可能是补充信息,应识别意图:

  • 停止类(停止/取消/停/stop/cancel→ 后端 cockpit_chat_v2.dspy 检测关键词,立即返回「已停止当前任务」
  • 补充/新任务 → 作为新对话轮次处理,不打断旧请求

实现:cockpit_chat_v2.dspy 中 send_message 入口加 stop_keywords 检测,匹配则直接 yield MdWidget 返回。

Pipeline 浏览器登录

CDP 浏览器测试 pipeline 需先登录:

  1. 导航到 https://pipeline.opencomputing.cn/rbac/user/login.ui
  2. 填入 username/password点击表单 Submit 按钮
  3. 或用 fetch('/rbac/user/up_login.dspy', {method:'POST', body:'username=admin&password=admin123&_webbricks_=1'}) POST
  4. 登录成功后 session 生效,可导航到 /pipeline-sdlc 渲染 cockpit

password_encode()ahserver/globalEnv.py,用 RC4 + 配置 key。登录路径必须带 /user/ 前缀。

登录踩坑rbac 登录读的是 pipeline.usersget_module_dbname('rbac') 固定返回 pipeline),不是 sage.users——两个库各有独立 users 表,改错库密码不生效。测试无密码时可重置:

mysql pipeline -e "UPDATE users SET password='<password_encode(新密码)的输出>' WHERE username='admin'"
# password_encode 输出可用服务器 python 跑 globalEnv.password_encode('test123') 得到

前端登录按钮是 DIVtextContent=='Submit'),不是 <button>,浏览器自动化需用 document.querySelectorAll('div') 找 Submit 点击。

工作空间浏览器

点击 cockpit 的「工作空间」按钮弹出文件浏览器:左侧目录树 + 右侧文件列表 + Wterm 编辑器。

详见 references/workspace-browser.md

文件上传AgentIO → dspy → agent

前端 bugbricks agent.jsAgentIO.user_inputedhr.post(url, {params}) 发送,而 jsoncall.js 对非 FormData 走 JSON.stringify(data)File 对象(add_files)被序列化成 {},文件内容丢失。修复:有 add_files 时改用 FormData

var files = params.add_files || [];
var send_params = params;
if (files.length > 0) {
    send_params = new FormData();
    Object.keys(params).forEach(k => { if (k !== 'add_files' && k !== 'file_names') send_params.append(k, params[k]); });
    files.forEach(f => send_params.append('file', f));
}
var resp = await hr.post(this.opts.url, {params: send_params});

改完需重新 build bricksbuild.shbricks/*.js 合并成 dist/bricks.js(前端加载的是 dist 打包版,不是源码)。

后端接收dspymultipart 的 file 字段 → params_kw.get('file') 是 web_pathFileStorage().realPath(web_path) 拿绝对路径;多文件时是 list。

docx 提取文本复用模式dspy 与 read_file 两处都要):

def _extract_text(path, name):
    ext = os.path.splitext(name)[1].lower()
    if ext in ('.txt','.md','.json','.csv','.py','.log','.yaml','.yml','.xml','.html','.ini'):
        return open(path, encoding='utf-8', errors='ignore').read()[:15000]
    if ext == '.docx':
        import zipfile, re
        xml = zipfile.ZipFile(path).read('word/document.xml').decode('utf-8','ignore')
        return '\n'.join(re.findall(r'<w:t[^>]*>(.*?)</w:t>', xml))[:15000]
    return ''

read_file 工具必须支持 docx_t_read_file 若用 open(full, encoding='utf-8') 强读 docx 会 UnicodeDecodeErroragent 读 docx 失败后会绕去 run_command 执行 file/unzip/pandoc 探测run_command requires_confirmation=True 会 confirm 卡死)。修复两处:

  1. _t_read_fileagent_loop_v2.py加 docx 分支 + 二进制友好提示。
  2. read_file 的 ToolDefinition.description 明确写"支持 docx自动解析提取正文"——光改 handler 不够LLM 不知道它能读 docx 就不会选它。

注入语义:文件内容作为中性上下文注入 prompt"用户上传了文件,内容如下…"),不硬编码"总结",由 agent 结合用户 prompt 决定动作。

常见陷阱

陷阱 现象 修复
_exec_tool 返回 widget JSON 被 _w_card 包裹 Conform/弹窗不显示 result.startswith('{"widgettype":') 检测,直接 yield
_save_ctx user_id=None 竞态 Duplicate entry 错误 UPDATE-first + try/except
表名/字段名不匹配 1054 Unknown column 查 models/*.json 确认实际字段名
create_project sd_iterations 字段名 name vs iteration_name 查 model 确认
deepseek reply 不调工具 空回复/问用户 auto-inject 兜底最多3次推
LLM 编造工具名 未知工具: get_tasks 加别名映射
工作空间弹出窗为空 点击弹空白窗口 先选项目VScrollPanel 替代 TreeTree API 不匹配)
start.sh restart 不生效 旧代码仍在运行 pkill -9 -f pipeline_app 强制杀进程