8.0 KiB
| name | description |
|---|---|
| cockpit-agent-dev | Cockpit agent v1 DSPY + v2 AgentExecutor patterns, pitfalls, tool-loop fixes. |
Cockpit Agent 开发规范
架构
- v1:
cockpit_chat.dspy— 会话 agent,LLM 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,不要再去改已删除的 v1(cockpit_chat.dspy)。历史遗留过两个入口:index.ui(AgentIO→v2)与 sd_cockpit/index.ui(TextFiles→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_id(SDLC 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_tasks、get_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 需先登录:
- 导航到
https://pipeline.opencomputing.cn/rbac/user/login.ui - 填入 username/password,点击表单 Submit 按钮
- 或用
fetch('/rbac/user/up_login.dspy', {method:'POST', body:'username=admin&password=admin123&_webbricks_=1'})POST - 登录成功后 session 生效,可导航到
/pipeline-sdlc渲染 cockpit
password_encode() 在 ahserver/globalEnv.py,用 RC4 + 配置 key。登录路径必须带 /user/ 前缀。
登录踩坑:rbac 登录读的是 pipeline.users(get_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') 得到
前端登录按钮是 DIV(textContent=='Submit'),不是 <button>,浏览器自动化需用 document.querySelectorAll('div') 找 Submit 点击。
工作空间浏览器
点击 cockpit 的「工作空间」按钮弹出文件浏览器:左侧目录树 + 右侧文件列表 + Wterm 编辑器。
详见 references/workspace-browser.md
文件上传(AgentIO → dspy → agent)
前端 bug:bricks agent.js 的 AgentIO.user_inputed 用 hr.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 bricks:build.sh 把 bricks/*.js 合并成 dist/bricks.js(前端加载的是 dist 打包版,不是源码)。
后端接收(dspy):multipart 的 file 字段 → params_kw.get('file') 是 web_path,FileStorage().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 会 UnicodeDecodeError,agent 读 docx 失败后会绕去 run_command 执行 file/unzip/pandoc 探测(run_command requires_confirmation=True 会 confirm 卡死)。修复两处:
_t_read_file(agent_loop_v2.py)加 docx 分支 + 二进制友好提示。- 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 替代 Tree(Tree API 不匹配) |
start.sh restart 不生效 |
旧代码仍在运行 | pkill -9 -f pipeline_app 强制杀进程 |