Solon v4.1.0

chat - 模型的响应与计费

</> markdown
2026年9月7日 上午8:33:28

在 Solon AI 中,模型调用有两种返回方式:call() 返回一个 ChatResponse,适用于同步调用;stream() 返回 Flux<ChatEvent>,适用于流式订阅。流式事件中的 RESPONSE_END 是一次响应的正常终态,调用 getResponse() 可取得完整的最终 ChatResponse(其 getMessage() 等结果访问器对应完整聚合结果)。ChatResponse 还负责追踪 Token 的消耗情况。

1、响应接口 (ChatResponse)

ChatResponse 统一了承载同步调用结果和流式终态结果的结构。针对目前流行的“推理模型”(如 DeepSeek-R1, OpenAI o1),它提供了专门的方法来区分思维链(Thinking)和最终答案。

提示: 对于支持深度思考的模型,推荐使用 getText() 获取最终文本,getContent() 可用于获取消息原始内容或日志记录。

以下接口代码与当前 solon-ai-core/src/main/java/org/noear/solon/ai/chat/ChatResponse.java 一致:

package org.noear.solon.ai.chat;

import org.noear.solon.ai.AiUsage;
import org.noear.solon.ai.chat.content.ContentBlock;
import org.noear.solon.ai.chat.event.ChatEvent;
import org.noear.solon.ai.chat.event.ChatEventType;
import org.noear.solon.ai.chat.message.AssistantMessage;
import org.noear.solon.ai.chat.tool.ToolCall;
import org.noear.solon.lang.Nullable;
import org.noear.solon.lang.Preview;

import java.util.List;

/**
 * 聊天响应(模型调用的结果,不可变)
 */
@Preview("3.1")
public interface ChatResponse {
    /**
     * 获取原始响应数据
     */
    @Nullable
    String getFrameRaw();

    /**
     * 是否为终态
     */
    boolean isTerminal();

    /**
     * 获取模型
     */
    String getModel();

    /**
     * 获取错误
     */
    @Nullable
    ChatException getError();

    /**
     * 获取消息(终态;流式下即完整聚合结果)
     */
    @Nullable
    AssistantMessage getMessage();

    /**
     * 是否为空(没有内容,也没有工具调用)
     */
    boolean isEmpty();

    /**
     * 是否有消息内容
     */
    boolean hasContent();

    /**
     * 获取消息原始内容
     */
    String getContent();

    /**
     * 获取文本
     */
    String getText();

    /**
     * 获取思考
     */
    String getThinking();

    /**
     * 获取工具调用(没有时为空集合,不为 null)
     *
     * @since 4.1
     */
    List<ToolCall> getToolCalls();

    /**
     * 获取内容块(多模态;没有时为空集合,不为 null)
     *
     * @since 4.1
     */
    List<ContentBlock> getBlocks();

    /**
     * 获取完成原因(已归一化:工具调用为 {@code "tool"}、正常结束为 {@code "stop"},
     * 其余如 {@code "length"} / {@code "content_filter"} 原样透传)
     *
     * @since 4.1
     */
    String getFinishReason();

    /**
     * 获取使用情况(完成时,才会有使用情况)
     */
    @Nullable
    AiUsage getUsage();

    /**
     * 本次响应的语义事件(仅非流式)
     *
     * <p>非流式 {@code call()} 没有事件流可供投递,但方言同样会解析出引用、服务端工具结果、
     * 思考签名、拒答等语义——这些信息并非流式独有,不能静默丢弃。因此非流式路径改为把事件
     * 收集到结果上。</p>
     *
     * <p>流式路径下事件由 {@code stream()} 直接投递,此处为空列表(不会为 null)。
     * 自动工具调用产生多轮时,与聚合消息一致——只携带末轮。</p>
     *
     * @since 4.1
     */
    List<ChatEvent> getEvents();
}

调用与流式订阅示例:

// 同步调用:直接取得最终响应
ChatResponse response = chatModel.prompt("hello").call();
System.out.println(response.getText());

// 流式调用:RESPONSE_END 的 getResponse() 是完整终态
chatModel.prompt("hello").stream().subscribe(event -> {
    if (event.is(ChatEventType.RESPONSE_END)) {
        ChatResponse terminal = event.getResponse();
        System.out.println(terminal.getText());
    }
});

2、计费与使用统计 (AiUsage)

AiUsage 用于记录单次对话消耗的 Token 资源。不同的模型服务商提供的原始 JSON 结构差异很大,Solon AI 将其标准化为三个核心指标。

package org.noear.solon.ai;

import org.noear.snack4.ONode;

public class AiUsage {
    private final long promptTokens;
    private final long thinkTokens;
    private final long completionTokens;
    private final long totalTokens;
    private final long cacheCreationInputTokens;
    private final long cacheReadInputTokens;
    private final ONode source;

    public AiUsage(long promptTokens, long thinkTokens, long completionTokens, long totalTokens, ONode source) {
        this(promptTokens, thinkTokens, completionTokens, totalTokens, 0L, 0L, source);
    }

    public AiUsage(long promptTokens, long thinkTokens, long completionTokens, long totalTokens,
                   long cacheCreationInputTokens, long cacheReadInputTokens, ONode source) {
        this.promptTokens = promptTokens;
        this.thinkTokens = thinkTokens;
        this.completionTokens = completionTokens;
        this.totalTokens = totalTokens;
        this.cacheCreationInputTokens = cacheCreationInputTokens;
        this.cacheReadInputTokens = cacheReadInputTokens;
        this.source = source;
    }

    /**
     * 获取提示语消耗词元数
     */
    public long promptTokens() {
        return promptTokens;
    }

    /**
     * 获取思考消耗词元数
     */
    public long thinkTokens() {
        return thinkTokens;
    }

    /**
     * 获取完成消耗词元数
     */
    public long completionTokens() {
        return completionTokens;
    }

    /**
     * 获取总消耗词元数
     */
    public long totalTokens() {
        return totalTokens;
    }

    /**
     * 获取缓存创建输入词元数 (Claude Prompt Caching)
     */
    public long cacheCreationInputTokens() {
        return cacheCreationInputTokens;
    }

    /**
     * 获取缓存读取输入词元数 (Claude Prompt Caching)
     */
    public long cacheReadInputTokens() {
        return cacheReadInputTokens;
    }

    /**
     * 源数据
     */
    public ONode getSource() {
        return source;
    }
}

3、代码示例

获取清理后的内容

ChatResponse resp = chatModel.call(prompt);

// 自动处理思考过程,只打印结果
System.out.println("Result: " + resp.getText());

// 打印计费信息
AiUsage usage = resp.getUsage();
if (usage != null) {
    System.out.println("Cost Tokens: " + usage.totalTokens());
}

处理原始数据 如果你需要获取厂商特有的计费细节(例如 DeepSeek 的 prompt_cache_hit_tokens):

long cacheHit = resp.getUsage().getSource()
                    .get("usage")
                    .get("prompt_cache_hit_tokens").getLong();