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 → 前端
数据流详细说明
WealthAssistantAgentFactory.build()创建ReActAgent,将 AgentRule.md 的 Markdown body 作为 system prompt 注入- 没有设置任何
responseFormat或outputConfig------ 因为 ReActAgent 不使用工作流 LLM 组件 - LLM 根据 AgentRule.md 第四节"执行总结"的文本模板输出最终答案
- ReActAgent 将 LLM 原始输出包装为
{output: content, result_type: "answer"} JiuwenCoreAgentHandler将其规范化为OutputSchema流块,传递给前端- 前端收到的最终内容是纯文本/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 含 output 和 result_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
调用链:
getModelInput()调用LLMPromptFormatter.formatPrompt()------ 注入格式指令invoke()或stream()调用 LLMstream()中根据responseFormatType分发:- JSON →
invokeForJsonFormat()(非流式单次调用,因为 JSON 需要完整内容才能解析) - text/markdown →
streamWithChunks()(流式分块)
- JSON →
- 最终都通过
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 请求发送 |