Browse Source

1. 优化了 Hermes Agent 执行上下文,HermesBridgeClient 在每次调用前扫描 HERMES_HOME/skills 注入「可用技能列表 + 高效执行策略」前置 prompt,引导 LLM 使用精确技能名并主动控制上下文膨胀,减少无效工具调用与重复探索;
2. 修复了 Hermes Agent 无法加载 Java 同步技能的根因,SkillFileSyncer 在复制 skill 目录时跳过 _versions 子目录,避免 hermes-agent 递归扫描时把版本快照下的 SKILL.md 也识别为候选,进而导致 skill_view 拒绝加载;
3. 修复了知识库文档上传时 “Value too long for column ERROR_MESSAGE CHARACTER VARYING(255)” 报错,KbDocument.errorMessage 字段长度由默认 255 扩到 1000,与 DocumentPipelineImpl 中已有的 1000 字符截断上限对齐,由 Hibernate ddl-auto=update 自动执行 ALTER COLUMN;
4. 更新了 .gitignore 和 prompt.md 需求记录。

weisijie 1 month ago
parent
commit
39f2ce967d

+ 1 - 0
.gitignore

@@ -62,6 +62,7 @@ temp/test/
 skills-manage/
 beekeeper-studio/
 SuperMew/
+agent-management-rag/
 
 # ===== 需求迭代记录 =====
 # prompt.md

+ 6 - 0
backend/src/main/java/com/agent/management/engine/SkillFileSyncer.java

@@ -73,6 +73,12 @@ public class SkillFileSyncer {
         Files.walkFileTree(src, new SimpleFileVisitor<>() {
             @Override
             public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
+                // 跳过 _versions 子目录:避免 hermes-agent 的 iter_skill_index_files 递归扫描时
+                // 把版本快照下的 SKILL.md 也识别为候选,触发 collision detection
+                // ("Ambiguous skill name") 导致 skill_view 拒绝加载。
+                if (dir.getFileName() != null && "_versions".equals(dir.getFileName().toString())) {
+                    return FileVisitResult.SKIP_SUBTREE;
+                }
                 Files.createDirectories(dst.resolve(src.relativize(dir)));
                 return FileVisitResult.CONTINUE;
             }

+ 101 - 7
backend/src/main/java/com/agent/management/engine/hermes/HermesBridgeClient.java

@@ -13,14 +13,19 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
 import org.springframework.stereotype.Component;
 
 import java.io.BufferedReader;
+import java.io.IOException;
 import java.io.InputStreamReader;
 import java.net.HttpURLConnection;
 import java.net.URI;
 import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
+import java.util.stream.Stream;
 
 /**
  * Hermes Bridge HTTP 客户端。
@@ -124,8 +129,10 @@ public class HermesBridgeClient {
         Map<String, Object> body = new ConcurrentHashMap<>();
         body.put("user_message", userMessage);
         body.put("max_iterations", maxIterations);
-        if (systemPrompt != null && !systemPrompt.isBlank()) {
-            body.put("system_prompt", systemPrompt);
+        // 注入「可用技能列表 + 高效执行策略」前置 prompt,减少 LLM 猜名失败与上下文膨胀
+        String enhancedSystemPrompt = buildEnhancedSystemPrompt(hermesHome, systemPrompt);
+        if (enhancedSystemPrompt != null && !enhancedSystemPrompt.isBlank()) {
+            body.put("system_prompt", enhancedSystemPrompt);
         }
         if (hermesHome != null && !hermesHome.isBlank()) {
             body.put("hermes_home", hermesHome);
@@ -177,7 +184,9 @@ public class HermesBridgeClient {
         conn.getOutputStream().flush();
 
         int responseCode = conn.getResponseCode();
-        long elapsed = System.currentTimeMillis() - startTime;
+        // connectMs = 建立连接 + 写出 body + 收到 HTTP 响应头的时间
+        // (注意:SSE 的响应头几乎立刻到达,body 还在流式传输,所以这里耗时很短)
+        long connectMs = System.currentTimeMillis() - startTime;
         if (responseCode != 200) {
             String error;
             try (var reader = new BufferedReader(new InputStreamReader(conn.getErrorStream(), StandardCharsets.UTF_8))) {
@@ -186,14 +195,20 @@ public class HermesBridgeClient {
                 while ((line = reader.readLine()) != null) sb.append(line);
                 error = sb.toString();
             }
-            LLM_HTTP.debug("[Hermes-RESP] HTTP {} ({}ms), error: {}", responseCode, elapsed, error);
+            long totalMs = connectMs;
+            LLM_HTTP.debug("[Hermes-RESP] HTTP {} (total={}ms), error: {}", responseCode, totalMs, error);
             throw new RuntimeException("Hermes Bridge 返回 HTTP " + responseCode + ": " + error);
         }
 
+        long streamStart = System.currentTimeMillis();
         HermesRunResult result = parseSseResponse(conn, nodeId, sink);
-        // === 响应 DEBUG 日志 ===
-        LLM_HTTP.debug("[Hermes-RESP] HTTP 200 ({}ms), finalText 长度={}, logs 数={}",
-                elapsed,
+        // streamMs = 读取 SSE 流式 body 的耗时(这才是真正的 Agent 执行时间,包含所有 LLM 调用与工具循环)
+        long streamMs = System.currentTimeMillis() - streamStart;
+        long totalMs = System.currentTimeMillis() - startTime;
+
+        // === 响应 DEBUG 日志(分别记录连接耗时与流式读取耗时,便于定位卡点) ===
+        LLM_HTTP.debug("[Hermes-RESP] HTTP 200, connect={}ms, stream={}ms, total={}ms, finalText 长度={}, logs 数={}",
+                connectMs, streamMs, totalMs,
                 result.getFinalText() == null ? 0 : result.getFinalText().length(),
                 result.getLogs() == null ? 0 : result.getLogs().size());
         LLM_HTTP.debug("[Hermes-RESP] finalText: {}", result.getFinalText());
@@ -285,4 +300,83 @@ public class HermesBridgeClient {
         if (text == null) return null;
         return text.length() <= maxLen ? text : text.substring(0, maxLen) + "...";
     }
+
+    /**
+     * 高效执行策略前置 prompt。
+     *
+     * <p>用于解决 Hermes Agent 执行慢的几个根因(基于日志分析):</p>
+     * <ul>
+     *   <li>② LLM 猜错技能名导致无效工具调用(readme-gen / git-commit-log-gen 等不存在)</li>
+     *   <li>③ 中间轮次过度消化工具结果,每轮都生成几百 token 的「思考总结」</li>
+     *   <li>④ read_file 整个大文件导致 in= token 雪球式膨胀(23k→50k)</li>
+     *   <li>⑤ 探索完成后仍在循环验证,多出 2-3 轮无意义 LLM 调用</li>
+     * </ul>
+     *
+     * <p>这是「软优化」——通过 prompt 引导 LLM 自主控制上下文,而非硬性截断。
+     * 对比修改 hermes-agent 内部的 trajectory_compressor,这种方式风险最低、维护成本最小。</p>
+     */
+    private static final String EFFICIENCY_PROMPT_TEMPLATE =
+            "=== 执行策略提示(必须严格遵守)===\n\n" +
+            "## 可用技能(skill_view 调用时请使用精确名,禁止猜测)\n%s\n\n" +
+            "## 高效执行原则\n" +
+            "1. 一次性产出:明确目标后,应一次性生成所有需要的文件/输出," +
+            "不要每完成一个小步骤就停下来生成中间总结,避免多消耗 1 轮 LLM 调用。\n" +
+            "2. 摘要工具优先:当文件较大(>200 行)时,优先使用 head_file / search_files / grep " +
+            "等摘要工具定位关键内容,避免直接 read_file 整个大文件——上下文膨胀会让后续每一轮都更慢。\n" +
+            "3. 控制探索深度:达到目标即停止,不要重复读取已知信息、反复验证;" +
+            "任务完成后用一句话告知「已完成」,不要生成冗长总结报告。\n" +
+            "4. 工具结果复用:上一轮已经读到的内容,本轮不要重复读取;" +
+            "如需引用,直接基于已读内容作答。\n\n" +
+            "## 注意\n" +
+            "上述原则是「在合适场景下」的优化指引。当任务确实需要逐项探索(如多文件批量分析)时," +
+            "可按需展开;但不要为了「显得认真」而增加无意义的中间步骤。\n";
+
+    /**
+     * 在原始 system_prompt 前追加「执行策略提示」。
+     *
+     * @param hermesHome  本次运行的 HERMES_HOME(其下应有 skills 子目录),用于扫描可用技能列表
+     * @param basePrompt  调用方提供的原始 system_prompt(如 SKILL.md 内容),可为 null
+     * @return 增强后的 system_prompt;若 basePrompt 为空且无可用技能列表,返回 null(不发 system_prompt)
+     */
+    private String buildEnhancedSystemPrompt(String hermesHome, String basePrompt) {
+        String skillsList = scanAvailableSkills(hermesHome);
+        String efficiencySection = String.format(EFFICIENCY_PROMPT_TEMPLATE, skillsList);
+
+        if (basePrompt == null || basePrompt.isBlank()) {
+            return efficiencySection;
+        }
+        return efficiencySection + "\n---\n\n" + basePrompt;
+    }
+
+    /**
+     * 扫描 HERMES_HOME/skills 目录,返回逗号分隔的技能 folderName 列表。
+     * 扫描失败时返回占位文案(不影响主流程)。
+     *
+     * <p>每次扫描开销很小(单次 Files.list,几十毫秒级),相对于 Hermes 整体上百秒的耗时可以忽略,
+     * 因此不做缓存——这也避免了「运行时新增技能但缓存未更新」的问题。</p>
+     */
+    private String scanAvailableSkills(String hermesHome) {
+        if (hermesHome == null || hermesHome.isBlank()) {
+            return "(未提供 hermes_home,无法列出)";
+        }
+        Path skillsDir = Paths.get(hermesHome, "skills");
+        if (!Files.isDirectory(skillsDir)) {
+            return "(skills 目录不存在于 " + skillsDir + ")";
+        }
+        List<String> names = new ArrayList<>();
+        try (Stream<Path> entries = Files.list(skillsDir)) {
+            entries.filter(Files::isDirectory)
+                    .map(p -> p.getFileName().toString())
+                    .filter(s -> !s.startsWith(".") && !s.equals("__pycache__"))
+                    .sorted()
+                    .forEach(names::add);
+        } catch (IOException e) {
+            log.warn("[HermesBridgeClient] 扫描技能目录失败: {}", e.getMessage());
+            return "(扫描失败: " + e.getMessage() + ")";
+        }
+        if (names.isEmpty()) {
+            return "(无)";
+        }
+        return String.join(", ", names);
+    }
 }

+ 3 - 1
backend/src/main/java/com/agent/management/model/entity/KbDocument.java

@@ -57,7 +57,9 @@ public class KbDocument {
     @Column(name = "vector_count")
     private Integer vectorCount = 0;
 
-    @Column(name = "error_message")
+    // 长度 1000:与 DocumentPipelineImpl 中 errorMessage 截断上限对齐,
+    // 255 字符对错误堆栈摘要过短,无法定位问题。
+    @Column(name = "error_message", length = 1000)
     private String errorMessage;
 
     @Column(name = "created_at")

+ 27 - 0
prompt.md

@@ -958,3 +958,30 @@ Caused by: java.lang.NumberFormatException: For input string: "undefined"
 ---
 
 OK,我查看了“智能润色”功能的输出,LLM已经返回了符合要求的结果,为什么前端界面未将prompt字段回填?
+
+---
+
+目前,Hermes的执行速度比较慢,请查看logs中的最后一次调用日志(含.log、-llm.log),对比时间,找出卡点,告诉我,我们一起来想优化方法。
+
+---
+
+分析最近2次的调用日志,看看为什么第一次时间没有大的提升;第二次更换模型后提升很多,有没有其他优化的点可以应用?
+
+---
+
+P0方案B,先看看java的技能为什么不被hermes识别
+
+---
+
+查看最近一次的任务日志,为什么时间又变长了很多?
+
+---
+
+接收现状。那么,根据日志分析,Hermes的初始化和启动时间长不长?需不需要做类似线程池的机制,让Hermes进程常驻?
+
+---
+
+文档上传报错:could not execute statement [Value too long for column "ERROR_MESSAGE CHARACTER VARYING(255)"]。
+
+---
+