【openJiuwen】大模型流式输出的格式控制探索及实现方案

openJiuwen 框架 SSE 流式输出 JSON 格式控制机制分析

一、核心结论

openJiuwen 框架控制 LLM 最终输出格式(JSON vs Markdown)存在两条完全不同的路径,取决于使用哪种 Agent 类型:

路径 Agent 类型 格式控制方式 适用场景
路径 A 工作流 LLM 组件 (LLMExecutable) 编程式:responseFormat + outputConfig + OutputFormatter + LLMPromptFormatter 工作流型 Agent
路径 B ReActAgent 提示词式:完全靠 system prompt (AgentRule.md) 控制 wealth-assistant 等应用

当前 wealth-assistant 使用的是 ReActAgent(路径 B),LLM 原始输出文本被直接透传给前端,不经过 JSON 解析/校验。


二、当前 wealth-assistant 的输出链路

复制代码
AgentRule.md (system prompt) → LLM → ReActAgent → OutputSchema → JiuwenCoreAgentHandler → SSE → 前端

数据流详细说明

  1. WealthAssistantAgentFactory.build() 创建 ReActAgent,将 AgentRule.md 的 Markdown body 作为 system prompt 注入
  2. 没有设置任何 responseFormatoutputConfig ------ 因为 ReActAgent 不使用工作流 LLM 组件
  3. LLM 根据 AgentRule.md 第四节"执行总结"的文本模板输出最终答案
  4. ReActAgent 将 LLM 原始输出包装为 {output: content, result_type: "answer"}
  5. JiuwenCoreAgentHandler 将其规范化为 OutputSchema 流块,传递给前端
  6. 前端收到的最终内容是纯文本/Markdown(带【需求概述】等中文标记),不是结构化 JSON

关键代码位置

ReActAgent.invoke 路径agent-core-java-feature-630/.../singleagent/agents/ReActAgent.java 第 719-726 行):

java 复制代码
Map<String, Object> result = new HashMap<>();
result.put("output", aiMessage.getContent());  // LLM 原始文本直接放入 output
result.put("result_type", "answer");

ReActAgent 流式路径 (第 1325-1342 行)writeStreamResult()

将最终结果包装为 OutputSchema("answer", 0, payload),payload 含 outputresult_type

关键:ReActAgent 的输出完全是 LLM 原始文本内容,不经过 OutputFormatter 处理,没有 JSON 解析/校验/字段提取。


三、框架中两条输出路径详细对比

路径 A:工作流 LLM 组件(支持 JSON 格式控制)

3.1 三种合法响应类型

文件 : agent-core-java-feature-630/.../workflow/component/llm/WorkflowLLMResponseType.java

java 复制代码
public enum WorkflowLLMResponseType {
    JSON("json"),
    MARKDOWN("markdown"),
    TEXT("text");
}
3.2 格式控制的双层机制

框架不向 LLM API 传递 response_format 参数,而是通过两层来控制:

第一层 ------ 提示词注入(发送前)

文件 : agent-core-java-feature-630/.../workflow/component/llm/LLMPromptFormatter.java

LLMPromptFormatter.formatPrompt() 在发送给 LLM 之前,修改最后一条 user message:

  • "text" → 不注入任何指令,原样返回
  • "markdown" → 注入 Markdown 格式指令
  • "json" → 注入 DEFAULT_JSON_INSTRUCTION,内容为 "Strictly return the answer in valid JSON format only..." 并附带由 outputConfig 生成的 JSON schema
java 复制代码
private static final String DEFAULT_JSON_INSTRUCTION =
    "Carefully consider the user's question to ensure your answer is logical and makes sense.\n"
    + "- Make sure your explanation is concise and easy to understand, not verbose.\n"
    + "- Strictly return the answer in valid JSON format only, and "
    + "\"DO NOT ADD ANY COMMENTS BEFORE OR AFTER IT\" to ensure it could be formatted "
    + "as a JSON instance that conforms to the JSON schema below.\n"
    + "Here is the JSON schema: ${json_schema}.\n"
    + "The question is: ${query}.";

第二层 ------ 响应后处理(返回后)

文件 : agent-core-java-feature-630/.../workflow/component/llm/OutputFormatter.java

OutputFormatter.formatResponse() 根据 responseFormat.get("type") 分发:

  • "text" / "markdown"formatTextResponse():将原始文本包装为 {fieldName: responseContent}
  • "json"formatJsonResponse():解析 JSON 内容,根据 outputConfig 进行 schema 校验和字段提取
3.3 调用编排 LLMExecutable

文件 : agent-core-java-feature-630/.../workflow/component/llm/LLMExecutable.java

调用链:

  1. getModelInput() 调用 LLMPromptFormatter.formatPrompt() ------ 注入格式指令
  2. invoke()stream() 调用 LLM
  3. stream() 中根据 responseFormatType 分发:
    • JSON → invokeForJsonFormat()非流式单次调用,因为 JSON 需要完整内容才能解析)
    • text/markdown → streamWithChunks()(流式分块)
  4. 最终都通过 OutputFormatter.formatResponse() 格式化输出
3.4 配置方式
java 复制代码
LLMCompConfig.builder()
    .responseFormat(Map.of("type", "json"))
    .outputConfig(outputConfigMap)
    .build();

路径 B:ReActAgent(当前使用,不支持格式控制)

特性 说明
格式控制 仅靠 system prompt 提示词
JSON 校验
流式输出 支持(JSON/Markdown 都流式)
responseFormat 配置 不支持
OutputFormatter 不经过
适用场景 对话型 Agent

四、JiuwenCoreAgentHandler 分析

文件 : runtime/.../adapters/agentcore/agentfw/JiuwenCoreAgentHandler.java

该处理器是一个运行时适配器,职责:

  • 启动/停止 AgentCore Runner
  • ServeRequest 转换为 Runner 输入
  • 调用 Runner.runAgentStreaming() 获取流式输出
  • OutputSchema 流块规范化为 {type, index, payload} 格式
  • 将中断信号映射为结构化中断数据
  • 组装 QueryResponse

该处理器中没有任何设置 LLM output format 的逻辑。 它使用 StreamMode.OUTPUT 模式,只是透明传递 Agent 的输出块。


五、AgentRule.md 中的输出格式指令

文件 : applications/wealth-assistant/src/main/resources/AgentRule.md

输出格式相关指令集中在两处:

YAML frontmatter 中的 summary 配置(第 89-96 行):

yaml 复制代码
summary:
  format: "需求概述→规划过程→任务执行情况→结果汇总→异常说明"
  max_length: 500
  required_fields:
    - 用户查询
    - 执行步骤
    - 结果状态

Markdown body 中的第四节(第 266-278 行):

复制代码
## 四、执行总结
所有任务完成或终止时,输出符合下面格式的最终答案:
【需求概述】<一句话>
【规划过程】<简述>
【任务执行情况】<每个 todo 的结果>
【结果汇总】<关键信息 / 笔记摘要 / 待办数量等>
【异常说明】<如有>
总长度 ≤ 500 字。

AgentRule.md 指定的是纯文本/Markdown 格式(用中文方括号标记的模板),不是 JSON 格式。


六、前端 SSE 接收机制

前端收到的数据结构

SSE 流式传输,每个帧的 data 是 JSON 格式:

复制代码
data: {"type":"answer","index":0,"payload":{"output":"...","result_type":"answer"}}

data: [DONE]

Chunk 类型

type 说明 前端处理
llm_reasoning LLM 推理 token 显示在思考面板
llm_output LLM 文本输出 流式累积到回答区
tool_call 工具调用详情 显示在思考面板
tool_result 工具返回结果 显示在思考面板
answer 最终全量回答 替换流式内容为权威全文
error 错误事件 追加错误提示
__interaction__ 人机交互中断 显示中断提示
llm_usage Token 用量 跳过不处理

最终回答内容格式

answer 类型 payload 中的 output 字段是 LLM 原始文本(Markdown 或纯文本),不是结构化 JSON。


七、实现 JSON 输出的可选方案

方案一:修改 AgentRule.md(最小改动)

在 system prompt 中强制要求 JSON 输出。

  • 优点:改动最小,仅需修改 AgentRule.md 中的"执行总结"部分
  • 缺点:LLM 不保证 100% 遵守,偶尔可能输出 Markdown
  • 适用:对格式严格度要求不高的场景

方案二:在 handler 层后处理

JiuwenCoreAgentHandler 中拦截 answer 类型的输出,尝试 JSON 解析。

  • 优点:不依赖 LLM 自觉性,可兜底
  • 缺点:需要修改 runtime 层代码
  • 适用:需要保证前端一定收到 JSON 的场景

方案三:切换到工作流 LLM 组件

使用 LLMCompConfig.builder().responseFormat(Map.of("type","json")).outputConfig(...) 配置。

  • 优点:框架级保证 JSON 格式,自动注入 JSON schema 指令 + 响应后解析校验
  • 缺点:架构改动大,需要重写 Agent 构建逻辑;JSON 模式为非流式调用
  • 适用:对 JSON 格式有强要求的正式生产环境

方案对比

维度 方案一:改 AgentRule.md 方案二:handler 后处理 方案三:切换工作流 LLM
改动范围 AgentRule.md runtime 层 Agent 构建逻辑
格式保证 LLM 自觉遵守 代码兜底解析 框架级保证
流式输出 保留 保留 JSON 模式非流式
实现复杂度
可靠性 较高 最高

建议:先用方案一验证 LLM 遵守度,如不够可靠再叠加方案二做兜底。方案三适合后续架构升级时考虑。


八、相关文件索引

核心文件

文件 作用
applications/wealth-assistant/src/main/resources/AgentRule.md system prompt,控制最终输出格式
applications/wealth-assistant/application.yml Agent 配置(LLM 参数、端口等)
runtime/.../JiuwenCoreAgentHandler.java 运行时适配器,透明传递输出
agent-core-java-feature-630/.../ReActAgent.java ReActAgent,LLM 原始输出直接透传
agent-core-java-feature-630/.../OutputSchema.java 流式输出块封装 {type, index, payload}

工作流 LLM 组件相关(路径 A)

文件 作用
.../workflow/component/llm/WorkflowLLMResponseType.java 三种响应类型枚举(JSON/MARKDOWN/TEXT)
.../workflow/component/llm/ResponseFormatConfig.java 响应格式配置校验
.../workflow/component/llm/LLMCompConfig.java LLM 组件配置(含 responseFormat/outputConfig)
.../workflow/component/llm/LLMPromptFormatter.java 提示词注入(JSON schema 指令)
.../workflow/component/llm/OutputFormatter.java 响应后处理(JSON 解析/校验/字段提取)
.../workflow/component/llm/SchemaGenerator.java 从 outputConfig 生成 JSON Schema
.../workflow/component/llm/LLMExecutable.java 调用编排
.../workflow/component/llm/LLMExecutableState.java 流式累积

前端相关

文件 作用
wealth-assistant-frontend-vue/src/stores/chat.js SSE chunk 解析与 UI 更新
wealth-assistant-frontend-vue/src/api/index.js SSE 请求发送
相关推荐
小虎AI生活1 小时前
别再花两万找广告公司了,你的下一条宣传片,AI一个下午能出
ai编程
HjhIron1 小时前
从零构建 AI Agent 的 Memory 记忆系统(上篇)—— 核心概念与上下文管理
ai编程
HjhIron2 小时前
从零构建 AI Agent 的 Memory 记忆系统(下篇)—— 基于 Milvus 的长期记忆与 RAG 检索
ai编程
_codeOH2 小时前
MCP Server 开发实战:从 0 到 1 构建自己的工具服务
人工智能·ai编程
VIP_CQCRE3 小时前
Visual Studio 接入 Ace Data Cloud:让 LMLocal 直接调用统一 AI 模型能力
ai编程·visual studio·openai兼容·ace data cloud·lmlocal
AINative软件工程4 小时前
LLM 应用的测试替身工程实践:用 Fake/Stub/Mock 让 AI 代码真正跑起来 CI
单元测试·llm·ai编程
adaierya4 小时前
用 AI 解决音频转换编程问题
开发语言·人工智能·python·分类·ai编程