OllamaJsonClient.java
3.34 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
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;
}
}
}