package com.xly.service; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.xly.config.TracingChatModelListener; import com.xly.util.OkHttpUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import java.time.Instant; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; /** * OpenAI 兼容协议(/v1/chat/completions)的**受约束 JSON**补全(constrained decoding)。 * *
用 {@code response_format={type:"json_schema", json_schema:{schema}}} 做语法约束解码, * 保证输出**一定**是 schema 合法的 JSON(Ollama 侧由 XGrammar 实现,违反 schema 的 token 概率直接置 0)—— * 这是根治「把产品名塞进客户字段」这类**槽位/参数幻觉**的关键手段。实测 qwen3:14b 在此模式下 * 意图/实体抽取 8/8 正确、~2-3s/次(reasoning_effort=none)。 * *
用于两处「窄而稳」的推理子任务:{@link IntentService}(意图+实体分类)与受约束槽位填充。
* 主对话/查询走 LangChain4j 工具循环。
*/
@Service
public class LlmJsonClient {
private static final Logger log = LoggerFactory.getLogger(LlmJsonClient.class);
private final ObjectMapper mapper;
private final TracingChatModelListener tracing;
private final OkHttpUtil http = OkHttpUtil.getInstance(10, 120, 30);
@Value("${llm.base-url}")
private String baseUrl;
@Value("${llm.api-key:ollama}")
private String apiKey;
@Value("${llm.chat-model}")
private String model;
public LlmJsonClient(ObjectMapper mapper, TracingChatModelListener tracing) {
this.mapper = mapper;
this.tracing = tracing;
}
/**
* 受约束 JSON 补全:低温度、response_format=json_schema、非流式。
*
* @param system 系统提示(角色 + 抽取规则)
* @param user 用户话
* @param schema JSON Schema(Map 结构,序列化进 response_format.json_schema.schema)
* @return 解析后的 JsonNode;失败返回 null(调用方须降级处理,绝不因它中断主流程)
*/
public JsonNode completeJson(String system, String user, Map