diff --git a/README.md b/README.md index 99c9c8e..8f16725 100644 --- a/README.md +++ b/README.md @@ -1,58 +1,65 @@ -# OPRO Prompt Optimizer +# System Prompt Generator ## 功能概述 -OPRO (Optimization by PROmpting) 是一个基于大语言模型的提示词优化系统。本项目实现了真正的 OPRO 算法,通过迭代优化系统指令(System Instructions)来提升 LLM 在特定任务上的性能。 +这是一个基于大语言模型的系统提示词(System Prompt)生成和迭代优化工具。通过简单的任务描述,自动生成高质量的系统指令,并支持基于用户选择的迭代改进。 ### 核心功能 -- **系统指令优化**:使用 LLM 作为优化器,基于历史性能轨迹生成更优的系统指令 -- **多轮迭代优化**:支持多轮优化,每轮基于前一轮的性能反馈生成新的候选指令 +- **智能指令生成**:根据任务描述自动生成多个高质量的系统指令候选 +- **迭代式改进**:基于用户选择的指令生成改进版本,避免被拒绝的方向 +- **角色定义格式**:所有生成的指令都以角色定义开头(如"你是一个..."),符合最佳实践 - **智能候选选择**:通过语义聚类和多样性选择,从大量候选中筛选出最具代表性的指令 -- **性能评估**:支持自定义测试用例对系统指令进行自动评估 -- **会话管理**:支持多个优化任务的并行管理和历史记录 +- **会话管理**:支持多个任务的并行管理和历史记录 +- **全面覆盖要求**:生成的指令全面覆盖任务的所有要求和细节,而非仅追求风格多样性 ### 用户界面 - **现代化聊天界面**:类似 Google Gemini 的简洁设计 - **侧边栏会话管理**:可折叠的侧边栏,支持多会话切换 -- **实时优化反馈**:每轮优化生成 3-5 个候选指令,用户可选择继续优化或执行 +- **实时生成反馈**:每轮生成 5 个候选指令,用户可选择继续优化或复制使用 - **模型选择**:支持在界面中选择不同的 LLM 模型 -## 主要优化改进 +## 核心特性 -### 1. 真正的 OPRO 实现 +### 1. 简单直观的工作流程 -原始代码实现的是查询重写(Query Rewriting),而非真正的 OPRO。我们添加了完整的 OPRO 功能: +不同于复杂的 OPRO 算法(需要测试用例和自动评估),本工具采用简单直观的迭代改进方式: -- **系统指令生成**:`generate_system_instruction_candidates()` - 生成多样化的系统指令候选 -- **性能评估**:`evaluate_system_instruction()` - 基于测试用例评估指令性能 -- **轨迹优化**:基于历史 (instruction, score) 轨迹生成更优指令 -- **元提示工程**:专门设计的元提示用于指导 LLM 生成和优化系统指令 +- **初始生成**:输入任务描述 → 生成 5 个全面的系统指令候选 +- **迭代改进**:选择喜欢的指令 → 生成基于该指令的改进版本,同时避免被拒绝的方向 +- **无需评分**:不需要测试用例或性能评分,完全基于用户偏好进行改进 -### 2. 性能优化 +### 2. 高质量指令生成 -- **候选池大小优化**:从 20 个候选减少到 10 个,速度提升约 2 倍 -- **智能聚类选择**:使用 AgglomerativeClustering 从候选池中选择最具多样性的 Top-K +- **角色定义格式**:所有指令以"你是一个..."开头,符合系统提示词最佳实践 +- **全面覆盖要求**:生成的指令全面覆盖任务的所有要求和细节 +- **清晰可执行**:指令清晰、具体、可执行,包含必要的行为规范和输出格式 +- **简体中文**:所有生成的指令使用简体中文 + +### 3. 性能优化 + +- **候选池大小优化**:生成 10 个候选,通过聚类选择 5 个最具多样性的 +- **智能聚类选择**:使用 AgglomerativeClustering 从候选池中选择最具代表性的指令 - **嵌入服务回退**:Xinference → Ollama 自动回退机制,确保服务可用性 -### 3. API 架构改进 +### 4. API 架构 -- **新增 OPRO 端点**: - - `POST /opro/create` - 创建 OPRO 优化任务 - - `POST /opro/generate_and_evaluate` - 生成并自动评估候选 - - `POST /opro/execute` - 执行系统指令 - - `GET /opro/runs` - 获取所有优化任务 - - `GET /opro/run/{run_id}` - 获取特定任务详情 -- **会话状态管理**:完整的 OPRO 运行状态跟踪(轨迹、测试用例、迭代次数) +- **核心端点**: + - `POST /opro/create` - 创建新任务 + - `POST /opro/generate_and_evaluate` - 生成初始候选 + - `POST /opro/refine` - 基于用户选择进行迭代改进 + - `GET /opro/sessions` - 获取所有会话 + - `GET /opro/runs` - 获取所有任务 +- **会话管理**:支持多会话、多任务的并行管理 - **向后兼容**:保留原有查询重写功能,标记为 `opro-legacy` -### 4. 前端界面重构 +### 5. 前端界面 - **Gemini 风格设计**:简洁的白色/灰色配色,圆角设计,微妙的阴影效果 - **可折叠侧边栏**:默认折叠,支持会话列表管理 - **多行输入框**:支持多行文本输入,底部工具栏包含模型选择器 -- **候选指令卡片**:每个候选显示编号、内容、分数,提供"继续优化"、"复制"、"执行"按钮 +- **候选指令卡片**:每个候选显示编号和内容,提供"继续优化"和"复制"按钮 - **简体中文界面**:所有 UI 文本和生成的指令均使用简体中文 ## 快速开始 @@ -97,19 +104,18 @@ uvicorn _qwen_xinference_demo.api:app --host 0.0.0.0 --port 8010 ### 访问界面 -- **OPRO 优化界面**:http://127.0.0.1:8010/ui/opro.html +- **系统指令生成器**:http://127.0.0.1:8010/ui/opro.html - **传统三栏界面**:http://127.0.0.1:8010/ui/ - **API 文档**:http://127.0.0.1:8010/docs - **OpenAPI JSON**:http://127.0.0.1:8010/openapi.json ### 使用示例 -1. **创建新会话**:在 OPRO 界面点击"新建会话"或侧边栏的 + 按钮 -2. **输入任务描述**:例如"将中文翻译成英文" -3. **查看候选指令**:系统生成 3-5 个优化的系统指令 -4. **继续优化**:点击"继续优化"进行下一轮迭代 -5. **执行指令**:点击"执行此指令"测试指令效果 -6. **复制指令**:点击"复制"按钮将指令复制到剪贴板 +1. **创建新会话**:在界面点击"新建会话"或侧边栏的 + 按钮 +2. **输入任务描述**:例如"帮我写一个专业的营销文案生成助手" +3. **查看候选指令**:系统生成 5 个全面的系统指令,每个都以角色定义开头 +4. **选择并改进**:点击喜欢的指令上的"继续优化"按钮,生成基于该指令的改进版本 +5. **复制使用**:点击"复制"按钮将指令复制到剪贴板,用于你的应用中 ## 配置说明 @@ -123,8 +129,8 @@ OLLAMA_HOST = "http://127.0.0.1:11434" DEFAULT_CHAT_MODEL = "qwen3:8b" DEFAULT_EMBED_MODEL = "qwen3-embedding:4b" -# OPRO 优化参数 -GENERATION_POOL_SIZE = 10 # 生成候选池大小 +# 生成参数 +GENERATION_POOL_SIZE = 10 # 生成候选池大小(生成10个,聚类选择5个) TOP_K = 5 # 返回给用户的候选数量 CLUSTER_DISTANCE_THRESHOLD = 0.15 # 聚类距离阈值 @@ -157,11 +163,30 @@ XINFERENCE_EMBED_URL = "http://127.0.0.1:9997/models/bge-base-zh/embed" ## API 端点 -### OPRO 相关(推荐使用) +### 会话管理 + +- `POST /opro/session/create` - 创建新会话 +- `GET /opro/sessions` - 获取所有会话 +- `GET /opro/session/{session_id}` - 获取会话详情 + +### 任务管理 + +- `POST /opro/create` - 在会话中创建新任务 + - 请求体:`{"session_id": "xxx", "task_description": "任务描述", "model_name": "qwen3:8b"}` + - 返回:`{"run_id": "xxx", "task_description": "...", "iteration": 0}` + +### 指令生成 + +- `POST /opro/generate_and_evaluate` - 生成初始候选指令 + - 请求体:`{"run_id": "xxx", "top_k": 5, "pool_size": 10}` + - 返回:`{"candidates": [{"instruction": "...", "score": null}, ...]}` + +- `POST /opro/refine` - 基于用户选择进行迭代改进 + - 请求体:`{"run_id": "xxx", "selected_instruction": "用户选择的指令", "rejected_instructions": ["被拒绝的指令1", "被拒绝的指令2"]}` + - 返回:`{"candidates": [{"instruction": "...", "score": null}, ...], "iteration": 1}` + +### 任务查询 -- `POST /opro/create` - 创建优化任务 -- `POST /opro/generate_and_evaluate` - 生成并评估候选 -- `POST /opro/execute` - 执行系统指令 - `GET /opro/runs` - 获取所有任务 - `GET /opro/run/{run_id}` - 获取任务详情 @@ -181,6 +206,37 @@ XINFERENCE_EMBED_URL = "http://127.0.0.1:9997/models/bge-base-zh/embed" 详细 API 文档请访问:http://127.0.0.1:8010/docs +## 工作原理 + +### 初始生成流程 + +1. 用户输入任务描述(如"帮我写一个专业的营销文案生成助手") +2. 系统使用 LLM 生成 10 个候选指令 +3. 通过语义嵌入和聚类算法选择 5 个最具多样性的候选 +4. 所有候选都以角色定义开头,全面覆盖任务要求 + +### 迭代改进流程 + +1. 用户选择喜欢的指令(如候选 #3) +2. 系统记录被拒绝的指令(候选 #1, #2, #4, #5) +3. 向 LLM 发送改进请求:"基于选中的指令生成改进版本,避免被拒绝指令的方向" +4. 生成新的 10 个候选,聚类选择 5 个返回 +5. 用户可以继续迭代或复制使用 + +### 与 OPRO 的区别 + +**OPRO(原始算法)**: +- 需要测试用例(如数学题的正确答案) +- 自动评分(如准确率 0.73, 0.81) +- 基于性能轨迹优化 +- 适用于有明确评估标准的任务 + +**本工具(简单迭代改进)**: +- 不需要测试用例 +- 不需要自动评分 +- 基于用户偏好改进 +- 适用于任意通用任务 + ## 常见问题 ### 1. 无法连接 Ollama 服务 @@ -198,11 +254,17 @@ ollama serve ### 3. 生成速度慢 -- 调整 `GENERATION_POOL_SIZE` 减少候选数量 +- 调整 `GENERATION_POOL_SIZE` 减少候选数量(如改为 6,返回 3 个) - 使用更小的模型(如 `qwen3:4b`) - 确保 Ollama 使用 GPU 加速 -### 4. 界面显示异常 +### 4. 生成的指令质量不高 + +- 提供更详细的任务描述 +- 多次迭代改进,选择最好的继续优化 +- 尝试不同的模型 + +### 5. 界面显示异常 硬刷新浏览器缓存: - **Mac**: `Cmd + Shift + R` diff --git a/_qwen_xinference_demo/api.py b/_qwen_xinference_demo/api.py index 6d3005f..862e59d 100644 --- a/_qwen_xinference_demo/api.py +++ b/_qwen_xinference_demo/api.py @@ -24,7 +24,8 @@ from .opro.session_state import ( from .opro.user_prompt_optimizer import generate_candidates from .opro.user_prompt_optimizer import ( generate_system_instruction_candidates, - evaluate_system_instruction + evaluate_system_instruction, + refine_instruction_candidates ) from .opro.ollama_client import call_qwen @@ -159,6 +160,15 @@ class OPROExecuteReq(BaseModel): model_name: Optional[str] = None +class OPRORefineReq(BaseModel): + """Request to refine based on selected instruction (simple iterative refinement, NOT OPRO).""" + run_id: str + selected_instruction: str + rejected_instructions: List[str] + top_k: Optional[int] = None + pool_size: Optional[int] = None + + # ============================================================================ # LEGACY ENDPOINTS (Query Rewriting - NOT true OPRO) # ============================================================================ @@ -696,3 +706,44 @@ def opro_execute(req: OPROExecuteReq): }) except Exception as e: raise AppException(500, f"Execution failed: {e}", "EXECUTION_ERROR") + + +@app.post("/opro/refine", tags=["opro-true"]) +def opro_refine(req: OPRORefineReq): + """ + Simple iterative refinement based on user selection (NOT OPRO). + + This generates new candidates based on the selected instruction while avoiding rejected ones. + No scoring, no trajectory - just straightforward refinement based on user preference. + """ + run = get_opro_run(req.run_id) + if not run: + raise AppException(404, "OPRO run not found", "RUN_NOT_FOUND") + + top_k = req.top_k or config.TOP_K + pool_size = req.pool_size or config.GENERATION_POOL_SIZE + + try: + candidates = refine_instruction_candidates( + task_description=run["task_description"], + selected_instruction=req.selected_instruction, + rejected_instructions=req.rejected_instructions, + top_k=top_k, + pool_size=pool_size, + model_name=run["model_name"] + ) + + # Update iteration counter + update_opro_iteration(req.run_id, candidates) + + # Get updated run info + run = get_opro_run(req.run_id) + + return ok({ + "run_id": req.run_id, + "iteration": run["iteration"], + "candidates": [{"instruction": c, "score": None} for c in candidates], + "task_description": run["task_description"] + }) + except Exception as e: + raise AppException(500, f"Refinement failed: {e}", "REFINEMENT_ERROR") diff --git a/_qwen_xinference_demo/opro/prompt_utils.py b/_qwen_xinference_demo/opro/prompt_utils.py index 1fdff2f..07833ef 100644 --- a/_qwen_xinference_demo/opro/prompt_utils.py +++ b/_qwen_xinference_demo/opro/prompt_utils.py @@ -56,14 +56,15 @@ def generate_initial_system_instruction_candidates(task_description: str, pool_s 目标任务描述: 【{task_description}】 -请根据以上任务,生成 {pool_size} 条高质量、风格各异的"System Instruction"候选指令。 +请根据以上任务,生成 {pool_size} 条高质量、全面的"System Instruction"候选指令。 要求: -1. 每条指令必须有明显不同的风格和侧重点 -2. 覆盖不同的实现策略(例如:简洁型、详细型、示例型、角色扮演型、步骤型等) -3. 这些指令应指导LLM的行为和输出格式,以最大化任务性能 -4. 每条指令单独成行,不包含编号或额外说明 -5. 所有生成的指令必须使用简体中文 +1. 每条指令必须以角色定义开头(例如:"你是一个..."、"你是..."等) +2. 每条指令必须全面覆盖任务的所有要求和细节 +3. 指令应清晰、具体、可执行,能够有效指导LLM完成任务 +4. 确保指令包含必要的行为规范、输出格式、注意事项等 +5. 每条指令单独成行,不包含编号或额外说明 +6. 所有生成的指令必须使用简体中文 生成 {pool_size} 条指令: """ @@ -120,11 +121,68 @@ def generate_optimized_system_instruction( 然后,生成 {pool_size} 条新的、有潜力超越 {highest_score:.4f} 分的System Instruction。 要求: -1. 每条指令必须有明显不同的改进策略 -2. 结合高分指令的优点,避免低分指令的缺陷 -3. 探索新的优化方向和表达方式 -4. 每条指令单独成行,不包含编号或额外说明 -5. 所有生成的指令必须使用简体中文 +1. 每条指令必须以角色定义开头(例如:"你是一个..."、"你是..."等) +2. 每条指令必须全面覆盖任务的所有要求和细节 +3. 结合高分指令的优点,避免低分指令的缺陷 +4. 指令应清晰、具体、可执行,能够有效指导LLM完成任务 +5. 每条指令单独成行,不包含编号或额外说明 +6. 所有生成的指令必须使用简体中文 生成 {pool_size} 条优化后的指令: """ + + +def refine_based_on_selection( + task_description: str, + selected_instruction: str, + rejected_instructions: List[str], + pool_size: int = None +) -> str: + """ + Simple refinement: Generate variations based on selected instruction while avoiding rejected ones. + + This is NOT OPRO - it's straightforward iterative refinement based on user preference. + No scoring, no trajectory, just: "I like this one, give me more like it (but not like those)." + + Args: + task_description: Description of the task + selected_instruction: The instruction the user selected + rejected_instructions: The instructions the user didn't select + pool_size: Number of new candidates to generate + + Returns: + Prompt for generating refined candidates + """ + import config + pool_size = pool_size or config.GENERATION_POOL_SIZE + + rejected_text = "" + if rejected_instructions: + rejected_formatted = "\n".join(f"- {inst}" for inst in rejected_instructions) + rejected_text = f""" +**用户未选择的指令(避免这些方向):** +{rejected_formatted} +""" + + return f""" +你是一个"System Prompt 改进助手"。 +目标任务描述: +【{task_description}】 + +**用户选择的指令(基于此改进):** +{selected_instruction} +{rejected_text} + +请基于用户选择的指令,生成 {pool_size} 条改进版本。 + +要求: +1. 每条指令必须以角色定义开头(例如:"你是一个..."、"你是..."等) +2. 保留用户选择指令的核心优点 +3. 每条指令必须全面覆盖任务的所有要求和细节 +4. 指令应清晰、具体、可执行,能够有效指导LLM完成任务 +5. 避免与未选择指令相似的方向 +6. 每条指令单独成行,不包含编号或额外说明 +7. 所有生成的指令必须使用简体中文 + +生成 {pool_size} 条改进后的指令: +""" diff --git a/_qwen_xinference_demo/opro/user_prompt_optimizer.py b/_qwen_xinference_demo/opro/user_prompt_optimizer.py index be4c464..0529a12 100644 --- a/_qwen_xinference_demo/opro/user_prompt_optimizer.py +++ b/_qwen_xinference_demo/opro/user_prompt_optimizer.py @@ -11,7 +11,8 @@ from .prompt_utils import ( refine_instruction, refine_instruction_with_history, generate_initial_system_instruction_candidates, - generate_optimized_system_instruction + generate_optimized_system_instruction, + refine_based_on_selection ) def parse_candidates(raw: str) -> list: @@ -147,3 +148,46 @@ def evaluate_system_instruction( correct += 1 return correct / total + + +def refine_instruction_candidates( + task_description: str, + selected_instruction: str, + rejected_instructions: List[str], + top_k: int = config.TOP_K, + pool_size: int = None, + model_name: str = None +) -> List[str]: + """ + Simple refinement: Generate new candidates based on user's selection. + + This is NOT OPRO - just straightforward iterative refinement. + User picks a favorite, we generate variations of it while avoiding rejected ones. + + Args: + task_description: Description of the task + selected_instruction: The instruction the user selected + rejected_instructions: The instructions the user didn't select + top_k: Number of diverse candidates to return + pool_size: Number of candidates to generate before clustering + model_name: Optional model name to use + + Returns: + List of refined instruction candidates + """ + pool_size = pool_size or config.GENERATION_POOL_SIZE + + # Generate the refinement prompt + meta_prompt = refine_based_on_selection( + task_description, + selected_instruction, + rejected_instructions, + pool_size + ) + + # Use LLM to generate refined candidates + raw = call_qwen(meta_prompt, temperature=0.9, max_tokens=1024, model_name=model_name) + + # Parse and cluster + all_candidates = parse_candidates(raw) + return cluster_and_select(all_candidates, top_k=top_k) diff --git a/frontend/opro.html b/frontend/opro.html index 01ad2ea..7e4b4e0 100644 --- a/frontend/opro.html +++ b/frontend/opro.html @@ -304,41 +304,55 @@ createNewRun(msg); } - async function handleContinueOptimize(selectedInstruction, selectedScore) { - if (!currentRunId || loading) return; + async function handleContinueOptimize(selectedInstruction, allCandidates) { + if (!currentRunId || loading || !selectedInstruction) return; - // First, evaluate the selected instruction to add it to trajectory - if (selectedInstruction) { - setLoading(true); - try { - // Add the selected instruction to trajectory - const res = await fetch(`${API_BASE}/opro/evaluate`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - run_id: currentRunId, - instruction: selectedInstruction - }) - }); - const data = await res.json(); + setLoading(true); + try { + // Get rejected instructions (all except the selected one) + const rejectedInstructions = allCandidates + .map(c => c.instruction) + .filter(inst => inst !== selectedInstruction); - if (!data.success) { - throw new Error(data.error || 'Failed to evaluate instruction'); - } + // Call the refinement endpoint + const res = await fetch(`${API_BASE}/opro/refine`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + run_id: currentRunId, + selected_instruction: selectedInstruction, + rejected_instructions: rejectedInstructions + }) + }); + const data = await res.json(); - console.log('Evaluated instruction, score:', data.data.score); - } catch (err) { - alert('评估指令失败: ' + err.message); - console.error('Error evaluating instruction:', err); - setLoading(false); - return; - } finally { - setLoading(false); + if (!data.success) { + throw new Error(data.error || 'Failed to refine instruction'); } - } - // Then generate new candidates based on updated trajectory - await generateCandidates(currentRunId); + // Add refined candidates to messages + const newMessage = { + role: 'assistant', + type: 'candidates', + iteration: data.data.iteration, + candidates: data.data.candidates + }; + + setMessages(prev => { + const updated = [...prev, newMessage]; + // Save to session messages + setSessionMessages(prevSessions => ({ + ...prevSessions, + [currentSessionId]: updated + })); + return updated; + }); + } catch (err) { + alert('优化失败: ' + err.message); + console.error('Error refining instruction:', err); + } finally { + setLoading(false); + } } function handleExecute(instruction) { @@ -537,7 +551,7 @@ ), React.createElement('div', { className: 'flex gap-2' }, React.createElement('button', { - onClick: () => handleContinueOptimize(cand.instruction, cand.score), + onClick: () => handleContinueOptimize(cand.instruction, msg.candidates), disabled: loading, className: 'px-4 py-2 bg-white border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 disabled:bg-gray-100 disabled:text-gray-400 disabled:cursor-not-allowed transition-colors text-sm font-medium' }, '继续优化'),