OllamaJsonClient.java 3.34 KB
package com.xly.service;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
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.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

/**
 * 直连 Ollama /api/chat 的**受约束 JSON**补全(constrained decoding)。
 *
 * <p>为什么绕开 LangChain4j:Ollama 的 {@code format=<JSON Schema>} 会用 XGrammar 做语法约束解码,
 * 保证输出**一定**是 schema 合法的 JSON(违反 schema 的 token 概率直接置 0)——这是根治「把产品名塞进客户字段」
 * 这类**槽位/参数幻觉**的关键手段。实测 qwen3:14b 在此模式下意图/实体抽取 8/8 正确、~2-3s/次(think=false)。
 *
 * <p>用于两处「窄而稳」的推理子任务:{@link IntentService}(意图+实体分类)与
 * {@link SlotFillService}(按表单真实字段做受约束的槽位填充)。主对话/查询仍走 LangChain4j 工具循环。
 */
@Service
public class OllamaJsonClient {

    private static final Logger log = LoggerFactory.getLogger(OllamaJsonClient.class);

    private final ObjectMapper mapper;

    @Value("${langchain4j.ollama.base-url}")
    private String baseUrl;

    @Value("${langchain4j.ollama.chat-model-name}")
    private String model;

    public OllamaJsonClient(ObjectMapper mapper) {
        this.mapper = mapper;
    }

    /**
     * 受约束 JSON 补全:think=false(快且稳定,实测无精度损失)、低温度、format=schema、非流式。
     *
     * @param system 系统提示(角色 + 抽取规则)
     * @param user   用户话
     * @param schema JSON Schema(Map 结构,直接序列化进 format)
     * @return 解析后的 JsonNode;失败返回 null(调用方须降级处理,绝不因它中断主流程)
     */
    public JsonNode completeJson(String system, String user, Map<String, Object> schema) {
        try {
            Map<String, Object> body = new LinkedHashMap<>();
            body.put("model", model);
            body.put("stream", false);
            body.put("think", false);
            body.put("format", schema);
            body.put("messages", List.of(
                    Map.of("role", "system", "content", system),
                    Map.of("role", "user", "content", user)));
            Map<String, Object> opts = new LinkedHashMap<>();
            opts.put("temperature", 0.1);
            opts.put("top_p", 0.9);
            opts.put("num_ctx", 8192); // 足够容纳字段清单/历史,避免长上下文时工具/JSON 退化
            body.put("options", opts);

            String json = mapper.writeValueAsString(body);
            String resp = OkHttpUtil.getInstance(10, 120, 30).postJson(baseUrl + "/api/chat", json);
            JsonNode root = mapper.readTree(resp);
            String content = root.path("message").path("content").asText("");
            if (content.isBlank()) {
                log.warn("ollama json completion: empty content");
                return null;
            }
            return mapper.readTree(content);
        } catch (Exception e) {
            log.warn("ollama json completion failed: {}", e.getMessage());
            return null;
        }
    }
}