一、项目背景

前面的群面系统已经具备了房间创建、AI 候选人自动发言、阶段流转、和 AI 报告与导出的能力。但是当前流程存在几个问题:
1.导出 AI 报告时前端长时间无反馈,由于模拟过程对话过长,导致报告生成 Agent 容易因为上下文截断、工具参数过大或外部知识不足导致结果不稳定
2.用户进入房间后不知道当前讨论题目是什么
3.想插入自己的观点时会和 AI 候选人抢话
4.Agent 回复中的动作描写会被 TTS 生硬朗读
 

因此本轮工作分为两条线:

  1. 重构报告生成 Agent 的数据输入、执行流程和工具能力,让复盘报告不再只是能生成,而是更快、更稳、有更充实的外部知识辅助ai生成。

  2. 补齐四个影响真实使用体验的问题,让房间页和复盘页可以顺畅跑完

二、本轮项目进度概览

本轮主要完成了五个方向的升级:

模块

改造内容

技术价值

房间题目引导

新增房间信息聚合接口,前端展示题目、阶段、时长,并用 sessionStorage 兜底

解决刷新/直达房间页上下文丢失问题

用户请求发言

引入 userTurnHolder 独占回合,WebSocket 新增 request_turn / turn_change

解决用户发言与 AI Agent 并发冲突

TTS 文本清洗

前后端统一剔除括号动作描写,仅朗读语义内容

保留展示原文,同时提升语音体验

AI 报告导出进度

新增 SSE 流式进度、心跳、前端悬浮日志

解决长任务“假死”和导出状态不可见

报告生成 Agent

构建 InferencePack 压缩上下文,接入 ReAct 工具流、容错导出和 Tavily 全网搜索

提升报告证据覆盖、稳定性和分析深度

当前代码中的报告链路如下:

用户点击导出 AI 分析报告
  ↓
ReportManusController /generate/stream 创建 SSE
  ↓
ReportManusService 写结构化底稿
  ↓
RoomInferencePackBuilder 构建推理包
  ↓
ReportManusAgent 多轮 ReAct
  ├─ 分析 inferencePack
  ├─ 调用 searchGroupInterviewKnowledge 搜索群面知识
  ├─ exportMarkdown 容错写入 Markdown
  └─ exportPdf / doTerminate
  ↓
失败时 DirectReportGenerator 单次 LLM 备用
  ↓
必要时本地模板降级

当前代码已经从功能可用推进到Agent 工程化可控:有上下文压缩、有工具调用约束、有进度可观测、有失败降级。

三、报告生成 Agent 优化一:从原始事件流到分层压缩

3.1 当前项目存在什么问题

原来的报告生成直接把事件流整体喂给 Agent,采取的做法是加载最近八十条事件。
这样有两个问题:
一是早期破冰、观点提出和中段冲突等早期的事件容易因模拟事件流过长而丢失
二是如果把全部发言原文直接塞进 prompt,上下文过长又会造成模型处理缓慢,影响速度和稳定性

3.2 原因是什么

群面数据不是普通聊天记录,它同时需要全局时间线、阶段节奏、关键发言、个人画像和评分信息。简单截断会丢全局,简单全量输入会严重浪费上下文窗口。

3.3 如何处理

我采用的方法是引入InferencePack推理包,把全量事件压缩成多层结构:

层次

内容

作用

timeline

所有事件的 seq、时间、阶段、发言人、类型、100 字预览

保证模型知道每条事件存在

highlights

最多 80 条高信息密度事件的完整正文

给模型提供可引用证据

phaseDigests

每阶段事件数、发言人数、活跃度摘要

让模型理解讨论节奏

speakerDigests

每个发言人的总字数、首尾/最长发言、highlightSeqs

支持按人写评价

evaluation

五维评估或 evaluation_not_ready

防止模型编造评分

通过这样的分层压缩,成功的把原始事件流变成“可推理的数据结构”。

3.4 关键代码

InferencePack 的字段如下,体现了当前的上下文压缩策略:

public class InferencePack {
    private int totalEventCount;
    private RoomMeta meta;
    private List<TimelineEntry> timeline;
    private List<EventDetail> highlights;
    private List<PhaseDigest> phaseDigests;
    private List<SpeakerDigest> speakerDigests;
    private Object evaluation;
}

构建入口先计算高亮索引,再复用这个索引填充 highlights 和 speakerDigest:

Set<Integer> selectedIdxSet = computeSelectedIndexes(events);

InferencePack pack = InferencePack.builder()
        .roomId(roomId)
        .reportType(reportType)
        .totalEventCount(events.size())
        .meta(buildMeta(ctx, profiles))
        .timeline(buildTimeline(events))
        .highlights(buildHighlightsFromIndexes(events, selectedIdxSet))
        .phaseDigests(buildPhaseDigests(events))
        .speakerDigests(buildSpeakerDigests(events, profiles, selectedIdxSet))
        .evaluation(buildEvaluationObject(evaluation))
        .build();

高亮选择规则不是随机抽样,而是有优先级:

// P1-A:每阶段前 N + 后 N 条 SPEAK 事件
Map<String, List<Integer>> phaseSpeakIndexes = new LinkedHashMap<>();
for (int i = 0; i < events.size(); i++) {
    DiscussionEvent e = events.get(i);
    if (!"SPEAK".equalsIgnoreCase(e.getEventType())) continue;
    String phase = e.getPhase() == null ? "" : e.getPhase();
    phaseSpeakIndexes.computeIfAbsent(phase, k -> new ArrayList<>()).add(i);
}

// P1-B:关键互动事件
if (KEY_EVENT_TYPES.contains(e.getEventType().toUpperCase(Locale.ROOT))) {
    selected.add(i);
}

// P1-C:长发言,P2:每人至少一条,P3:剩余按长度补足

其中一个关键修复是:阶段首尾高亮只选 SPEAK,避免 ROOM_STARTPHASE_CHANGE 这种系统事件占掉高亮名额。

另一个关键修复是 highlightSeqs,让模型在写个人评价时能快速定位该用户的关键证据:

List<Integer> highlightSeqs = idxList.stream()
        .filter(selectedIdxSet::contains)
        .map(idx -> idx + 1)
        .collect(Collectors.toList());

四、报告生成 Agent 优化二:ReAct 主路径与工具调用约束

4.1 存在什么问题

当前报告生成 Agent 最大的不稳定点来自工具调用:模型可能只输出文本不调工具;可能在没有搜索外部知识前直接导出;可能重复读取事件流;也可能提前调用 doTerminate,导致接口看似执行完成但没有生成文件。

4.2 原因是什么

Tool Calling 不是传统函数调用,它依赖模型遵守系统提示。只靠 prompt 约束不够,工程侧还需要显式状态机、工具调用历史检查和纠偏消息。

4.3 如何处理

当前 ReportManusAgent 被设计成多轮 ReAct Agent,maxSteps=6,明确要求:

  1. 先分析预加载的 inferencePack

  2. 至少调用一次 searchGroupInterviewKnowledge

  3. 只有 exportFormat=pdf 时才调用 exportPdf

  4. 最后调用 doTerminate

ToolCallManusAgent 中,我加入了工具调用历史检查、重复工具阻断、缺失工具纠偏和导出前知识搜索校验。

4.4 关键代码

Agent 系统提示将报告生成拆成强制多步工作流:

setSystemPrompt("""
You are ReportManus, an expert group-interview replay analyst and coach.
You work as a multi-step ReAct agent with tools — NOT a single-shot completion.

Step 1 — Analyze: Review PreloadedData.
Step 2 — Search: Call searchGroupInterviewKnowledge at least ONCE.
Step 4 — Export: Call exportMarkdown with the FULL Chinese report.
Step 5 — Finish: Call exportPdf ONLY if exportFormat=pdf, then call doTerminate.

Do NOT call exportMarkdown before completing Step 2.
""");

如果模型想在未搜索时导出,代码侧直接阻断:

private boolean shouldBlockExportWithoutKnowledgeSearch(List<AssistantMessage.ToolCall> toolCalls) {
    boolean wantsExport = toolCalls.stream()
            .anyMatch(call -> markdownToolName.equals(call.name()) || pdfToolName.equals(call.name()));
    return wantsExport && !hasToolCallInHistory(SEARCH_KNOWLEDGE_TOOL);
}

如果模型一直重复读事件或评分,也会被阻断,防止 ReAct 空转:

private boolean shouldBlockRepeatedReadTools(List<AssistantMessage.ToolCall> toolCalls) {
    long eventCalls = countToolCallInHistory(LOAD_EVENT_STREAM_TOOL);
    long evalCalls = countToolCallInHistory(LOAD_EVALUATION_TOOL);
    boolean evalReady = isEvaluationReadyInHistory();

    boolean onlyReadTools = toolCalls.stream()
            .map(AssistantMessage.ToolCall::name)
            .allMatch(name -> LOAD_ROOM_CONTEXT_TOOL.equals(name)
                    || LOAD_EVENT_STREAM_TOOL.equals(name)
                    || LOAD_EVALUATION_TOOL.equals(name));

    if (askEventAgain && eventCalls >= MAX_EVENT_STREAM_CALLS) {
        return true;
    }
    return askEvalAgain && !evalReady && evalCalls >= MAX_EVALUATION_CALLS_WHEN_NOT_READY;
}

终止工具也不是无条件生效,必须确认导出已完成:

boolean requirePdf = isPdfRequested();
boolean hasMarkdownExport = hasToolCallInHistory(markdownToolName);
boolean hasPdfExport = hasToolCallInHistory(pdfToolName);
boolean exportSatisfied = requirePdf ? (hasMarkdownExport && hasPdfExport) : hasMarkdownExport;

if (exportSatisfied) {
    setState(ManusAgentState.FINISHED);
} else {
    getMessageList().add(new UserMessage(
            "Do not terminate yet. You must export markdown first, then call doTerminate."));
}

这说明本轮对 Agent 的处理已经不是“写一个 prompt 让它做事”,而是围绕 Agent 行为建立了工程约束。


五、报告生成 Agent 优化四:Tavily 全网搜索工具

5.1 存在什么问题

复盘报告不能只复述本场发言,还应该结合群面评审标准、无领导小组讨论技巧、领导力/协作/表达等通用框架给出建议。
纯 prompt 内置知识可以覆盖一部分,但对于“沉默者如何改进”“过度主导如何收敛”“群面领导力评分标准”等问题,外部知识搜索能让建议更有依据。

5.2 原因是什么

报告生成 Agent 的事实依据必须来自本场 inferencePack,但建议和 coaching 框架可以参考外部最佳实践。原链路没有全网搜索工具,模型只能依赖内置知识。

5.3 如何处理

我新增了 GroupInterviewKnowledgeTool,通过 Tavily Search API 检索群面相关知识,并把它注册进 reportManusTools。当前 ReAct 主路径要求至少调用一次 searchGroupInterviewKnowledge,同时工具描述中明确约束:不得用它查询本次房间数据,本场事实只能来自 inferencePack

同时,工具在没有配置 API Key 或请求失败时不会抛异常打断报告生成,而是返回降级提示,让 Agent 继续基于内置框架和本场数据完成报告。

5.4 关键代码

工具定义明确区分“外部知识”和“本场事实”:

@Tool(description = "搜索群面(Group Interview / 无领导小组讨论)相关知识,"
        + "包括评审标准、最佳实践、常见失分点、MBTI 角色表现、STAR 法则、领导力技巧等。"
        + "仅在需要引用外部知识来支撑对用户的建议时调用。"
        + "不得用于查询本次面试房间的数据(请使用 inferencePack 数据)。")
public String searchGroupInterviewKnowledge(
        @ToolParam(description = "搜索关键词,中文或英文均可。")
        String query) {
    if (tavilyApiKey == null || tavilyApiKey.isBlank()) {
        return buildNoKeyResponse(query);
    }
    // POST https://api.tavily.com/search
}

请求 Tavily 时只取必要结果,并限制每条内容长度,避免搜索结果反向膨胀上下文:

Map<String, Object> requestBody = new HashMap<>();
requestBody.put("api_key", tavilyApiKey);
requestBody.put("query", query);
requestBody.put("search_depth", "basic");
requestBody.put("include_answer", true);
requestBody.put("max_results", MAX_RESULTS);

解析返回时保留摘要答案、标题、内容摘要和来源:

JsonNode answerNode = root.get("answer");
if (answerNode != null && !answerNode.isNull() && !answerNode.asText().isBlank()) {
    sb.append("**摘要答案**:").append(answerNode.asText()).append("\n\n");
}

JsonNode results = root.get("results");
for (JsonNode result : results) {
    String title = getTextSafe(result, "title");
    String content = getTextSafe(result, "content");
    String url = getTextSafe(result, "url");
    if (content.length() > MAX_CONTENT_LEN) {
        content = content.substring(0, MAX_CONTENT_LEN) + "…";
    }
}

工具注册到报告 Agent 的工具集:

@Bean(name = "reportManusTools")
public ToolCallback[] reportManusTools(
        MarkdownExportTool markdownExportTool,
        PdfExportTool pdfExportTool,
        TerminateReportTool terminateReportTool,
        GroupInterviewKnowledgeTool groupInterviewKnowledgeTool
) {
    return ToolCallbacks.from(
            markdownExportTool,
            pdfExportTool,
            terminateReportTool,
            groupInterviewKnowledgeTool
    );
}

外部搜索不是直接替代本场数据,而是作为“建议框架”的补充来源;事实归事实,知识归知识,避免报告出现看似专业但脱离本场证据的问题。


六、问题一:房间内缺少题目引导

6.1 存在什么问题

用户从首页创建房间后,选择大方向题目却不清楚具体讨论问题的方向,进入讨论页后只能看到当前阶段和倒计时,看不到完整题目、题目类型、讨论时长等信息。如果刷新页面或直接打开房间 URL,前端也没有稳定的数据来源恢复题目内容。

这个问题会直接影响群面体验:用户不知道讨论背景,只能看 AI 候选人发言推测题目。

6.2 原因是什么

原有房间页依赖创建房间时的页面状态,但房间页本身没有一个“按 roomId 拉取完整房间上下文”的接口。后端虽然在 RoomContext 中保存了 configquestionContentcurrentPhase,但没有暴露给前端。

6.3 如何处理

我新增了只读聚合接口 GET /api/room/{roomId}/info,从 RoomContextQuestionRepository 汇总房间状态、当前阶段、题目元数据、难度、人数和时长。前端 RoomView 挂载时主动调用该接口,渲染可折叠题目面板;如果接口失败,再读取创建房间时写入的 sessionStorage 作为兜底。

6.4 关键代码

后端接口不是简单返回题目字符串,而是把房间运行态和题库元数据统一打包:

@GetMapping("/room/{roomId}/info")
public Map<String, Object> getRoomInfo(@PathVariable String roomId) {
    var context = roomService.getRoomContext(roomId);
    if (context == null) {
        return Map.of("success", false, "message", "房间不存在");
    }

    Map<String, Object> response = new HashMap<>();
    response.put("success", true);
    response.put("roomId", roomId);
    response.put("status", context.getStatus());

    if (context.getCurrentPhase() != null) {
        response.put("phase", context.getCurrentPhase().name());
        response.put("phaseName", context.getCurrentPhase().getName());
    }

    if (context.getConfig() != null) {
        var config = context.getConfig();
        response.put("difficulty", config.getDifficulty());
        response.put("agentCount", config.getAgentCount());
        response.put("duration", config.getDuration());
        response.put("targetPosition", config.getTargetPosition());
        // 根据 questionId 查询完整题目信息
    }

    return response;
}

前端房间页则把题目作为房间运行上下文展示,而不是只依赖首页传参:

async function loadRoomInfo() {
  const info = await getRoomInfo(roomId)
  if (info.success) {
    roomInfo.value = {
      question: info.question || null,
      phaseName: info.phaseName || '',
      duration: info.duration || null
    }
    if (info.phaseName) {
      currentPhase.value = info.phaseName
    }
    return
  }

  const cached = sessionStorage.getItem(`roomQuestion:${roomId}`)
  if (cached) {
    roomInfo.value = { question: JSON.parse(cached), phaseName: '', duration: null }
  }
}


七、问题二:用户发言与 AI Agent 并发冲突

7.1 存在什么问题

用户想参与讨论时,原来只能直接输入或打断。当前的用户请求发言功能并没有实现,这样会出现两个问题:

一是用户发言和 AI 候选人自动发言可能同时发生,消息顺序混乱;二是前端没有明确状态告诉用户“现在是否轮到你说话”,导致交互像普通聊天室,而不是群面里的发言权流转。

7.2 原因是什么

原有 Agent 循环只根据候选人的发言分数选择下一位发言者,没有“用户独占回合”这个状态。WebSocket 也缺少请求发言的协议,只能接收 speakinterrupt

7.3 如何处理

我在 RoomContext 中增加 userTurnHolder,表示当前持有用户发言权的人。用户点击“请求发言”后,前端发送 request_turn;后端授予发言权后广播 turn_change。在用户回合内,AgentEngine 会暂停候选人裁决循环,并清理当前正在发言状态。用户发送文本后,后端释放回合,广播 userId=agents,AI 候选人恢复自动发言。

7.4 关键代码

房间上下文增加用户回合状态:

/** 当前持有用户独占发言权的 userId,null 表示无 */
private String userTurnHolder;

授予发言权时,不只是写状态,还会暂停 Agent 当前发言:

public boolean grantUserTurn(String roomId, String userId) {
    RoomContext context = rooms.get(roomId);
    if (context == null || userId == null || userId.isBlank()) {
        return false;
    }
    if (context.getUserTurnHolder() != null && !context.getUserTurnHolder().equals(userId)) {
        return false;
    }
    context.setUserTurnHolder(userId);
    agentEngine.pauseAgentsForUser(roomId);
    broadcastTurnChange(roomId, userId);
    return true;
}

Agent 主循环中加入用户回合检查:

if (roomOps.isUserTurnActive(roomId)) {
    Thread.sleep(300);
    continue;
}

发言前也二次检查,避免异步延迟导致 Agent 抢在用户前面说话:

if (roomOps.isUserTurnActive(roomId)) {
    speakingInProgress.remove(roomId);
    currentSpeaker.remove(roomId);
    return;
}

WebSocket 协议层新增 request_turn,用户发言后自动释放回合:

case "request_turn" -> handleRequestTurn(wsMessage, roomService);

private void handleSpeak(WebSocketMessage message, RoomOperations roomService) {
    boolean isUser = isHumanSpeaker(userId, roomService, roomId);
    roomService.processEvent(...);
    broadcastToRoom(roomId, ...);
    if (isUser) {
        roomService.releaseUserTurn(roomId, userId);
    }
}

这补上了群面系统里的回合控制协议,让人类用户和 AI Agent 可以共享同一个讨论场;事件流中用户发言边界清晰,有利于 InferencePack 高亮与个人评价。


八、问题三:TTS 会朗读括号里的动作描写

8.1 存在什么问题

Agent 的回复经常包含 (点头)(思考片刻)【停顿】 这样的动作描写。展示在聊天气泡里没有问题,但如果 TTS 直接朗读,就会变成“点头、思考片刻、我认为……”,听感很差。

8.2 原因是什么

展示文本和朗读文本共用了同一个 content。而 Agent 输出里的括号动作本质上是舞台提示,不属于用户需要听到的语义内容。

8.3 如何处理

我没有修改原始消息内容,而是在朗读层增加文本清洗工具。后端 Edge TTS 和前端浏览器 TTS 都使用同一类规则:剔除中文圆括号、英文圆括号和中文方括号内的内容,再压缩多余空格。

这样做的好处是:聊天记录和报告证据仍保留完整原文,语音播放只消费适合朗读的文本。

8.4 关键代码

后端 TTS 清洗:

public static String forSpeech(String raw) {
    if (raw == null || raw.isBlank()) {
        return "";
    }
    return raw
            .replaceAll("([^)]*)", "")
            .replaceAll("\\([^)]*\\)", "")
            .replaceAll("【[^】]*】", "")
            .replaceAll("\\s{2,}", " ")
            .trim();
}

Agent 发言时,展示仍用完整 response,TTS 用清洗后的 ttsText

String ttsText = SpeechTextUtil.forSpeech(response);
if (ttsText.isBlank()) {
    ttsText = response;
}
TtsResponse tts = speechService.synthesize(
        TtsRequest.builder().text(ttsText).build());

前端手动朗读也复用同样规则:

export function textForSpeech(raw) {
  if (!raw || typeof raw !== 'string') return ''
  return raw
    .replace(/([^)]*)/g, '')
    .replace(/\([^)]*\)/g, '')
    .replace(/【[^】]*】/g, '')
    .replace(/\s{2,}/g, ' ')
    .trim()
}

这体现了“展示数据”和“消费数据”的分层:同一条消息可以服务聊天展示、持久化复盘和语音播放,但每个场景拿到的文本形态不同——与报告 Agent 侧“展示原文 / 消费清洗”的分层思路一致。


九、问题四:AI 报告导出没有过程反馈

9.1 存在什么问题

AI 报告生成是长任务,涉及写底稿、构建推理包、调用大模型、执行工具、写文件和可能的 PDF 导出。原来的同步接口会让用户点击后等待几十秒,期间页面没有可信反馈;如果模型卡住或工具失败,用户只能看到最终失败。

9.2 原因是什么

报告生成链路本身是多阶段的,但接口设计是“一次请求、一次响应”。后端没有把 Agent 思考、工具调用和文件写入这些中间态暴露给前端;前端也没有 SSE 消费能力。

9.3 如何处理

我新增了 POST /api/report/manus/generate/stream,后端用 SseEmitter 推送进度事件;ReportManusServiceToolCallManusAgent 在关键节点发出 startfactsagentllmtool_calltool_resultheartbeatcomplete 等事件。前端用 fetch + ReadableStream 解析 SSE,并在导出按钮旁展示可关闭的悬浮日志。

9.4 关键代码

后端接口设置 SSE 必要响应头,避免代理缓冲:

@PostMapping(value = "/generate/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter generateStream(@RequestBody GenerateRequest request, HttpServletResponse response) {
    response.setHeader("Cache-Control", "no-cache, no-transform");
    response.setHeader("X-Accel-Buffering", "no");
    response.setHeader("Connection", "keep-alive");

    SseEmitter emitter = new SseEmitter(300000L);
    reportManusService.generateReportStream(
            request.getRoomId(), reportType, format, request.getExtraPrompt(), emitter);
    return emitter;
}

服务层启动异步任务,并每 12 秒发送心跳,避免前端误判为卡死:

ScheduledFuture<?> heartbeat = heartbeatScheduler.scheduleAtFixedRate(() -> {
    sink.accept(ReportProgressEvent.of("heartbeat", "仍在处理中,请稍候…"));
}, 12, 12, TimeUnit.SECONDS);

sink.accept(ReportProgressEvent.of("start", "开始生成 AI 复盘报告…"));
sink.accept(ReportProgressEvent.of("facts", "正在写入结构化事实底稿…"));
ReportGenerationResult result = generateReport(roomId, reportType, exportFormat, extraPrompt, sink);

Agent 在 think() 和工具选择处主动上报进度:

emitProgress(ReportProgressEvent.builder()
        .phase("agent_step")
        .message("第 " + getCurrentStep() + "/" + getMaxSteps() + " 步:模型思考中…")
        .step(getCurrentStep())
        .totalSteps(getMaxSteps())
        .timestamp(System.currentTimeMillis())
        .build());

emitProgress(ReportProgressEvent.builder()
        .phase("llm")
        .message("第 " + getCurrentStep() + " 步:正在请求 DashScope 大模型…")
        .build());

for (AssistantMessage.ToolCall call : toolCalls) {
    emitProgress(ReportProgressEvent.toolCall(getCurrentStep(), getMaxSteps(), call.name()));
}

前端解析 SSE 块,不依赖浏览器原生 EventSource,因此可以用 POST 和鉴权头:

export async function generateManusReportStream(payload, onEvent) {
  const response = await fetch('/api/report/manus/generate/stream', {
    method: 'POST',
    headers,
    body: JSON.stringify(payload)
  })

  const reader = response.body.getReader()
  const decoder = new TextDecoder()
  let buffer = ''

  while (true) {
    const { done, value } = await reader.read()
    if (done) break
    buffer += decoder.decode(value, { stream: true })
    const parts = buffer.split(/\r?\n\r?\n/)
    buffer = parts.pop() || ''
    for (const block of parts) {
      dispatchSseBlock(block, onEvent)
    }
  }
}

这一步的价值是可观测性:用户看到的不再是一个转圈按钮,而是能知道系统正在写底稿、构建 InferencePack、调用 ReAct、执行 Tavily 搜索还是写入导出文件——与第三节至第七节 Agent 改造形成前后端闭环。


十、总结

本周工作把群面系统从“AI 候选人可以自动讨论”推进到“用户可以真实参与、报告可以稳定生成、Agent 行为可以被观察和约束”的阶段。

报告生成 Agent 的优化:解决的是工程能力上的瓶颈:上下文如何压缩、关键事实如何保留、工具如何稳定执行、外部知识如何接入(Tavily)、长任务如何让用户可感知

功能修复:解决的是用户体验上的问题:具体题目不可见、用户无法实际参与、TTS错误读出动作信息、ai导出过程不可视,它们分别向上游补充房间信息、向下游保证事件与展示质量,并为 Agent 报告链路提供可恢复上下文与可观测执行过程。

本轮改造更接近 Agent 工程化:用 InferencePack 管理上下文,用 ReAct 和工具调用实现多步任务,用 Tavily 引入外部知识,用 SSE 暴露执行过程,用容错解析和备用路径保证最终产物能够落盘。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐