feat(aihr): expose model usage telemetry
This commit is contained in:
+10
-1
@@ -70,6 +70,14 @@ public final class AihrModelDto {
|
|||||||
public record ChatRequest(String prompt, String model, String systemPrompt) {
|
public record ChatRequest(String prompt, String model, String systemPrompt) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public record ModelUsage(
|
||||||
|
Integer promptTokens,
|
||||||
|
Integer completionTokens,
|
||||||
|
Integer totalTokens,
|
||||||
|
long latencyMs
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
public record ChatResponse(
|
public record ChatResponse(
|
||||||
boolean configured,
|
boolean configured,
|
||||||
String providerCode,
|
String providerCode,
|
||||||
@@ -77,7 +85,8 @@ public final class AihrModelDto {
|
|||||||
String answer,
|
String answer,
|
||||||
String mode,
|
String mode,
|
||||||
String error,
|
String error,
|
||||||
List<String> hints
|
List<String> hints,
|
||||||
|
ModelUsage usage
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+30
-8
@@ -14,6 +14,7 @@ import org.dromara.aihr.domain.AihrModelDto.ConfigResponse;
|
|||||||
import org.dromara.aihr.domain.AihrModelDto.ProviderRequest;
|
import org.dromara.aihr.domain.AihrModelDto.ProviderRequest;
|
||||||
import org.dromara.aihr.domain.AihrModelDto.ProviderResponse;
|
import org.dromara.aihr.domain.AihrModelDto.ProviderResponse;
|
||||||
import org.dromara.aihr.domain.AihrModelDto.ProviderStatusRequest;
|
import org.dromara.aihr.domain.AihrModelDto.ProviderStatusRequest;
|
||||||
|
import org.dromara.aihr.domain.AihrModelDto.ModelUsage;
|
||||||
import org.dromara.common.satoken.utils.LoginHelper;
|
import org.dromara.common.satoken.utils.LoginHelper;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.dao.DataAccessException;
|
import org.springframework.dao.DataAccessException;
|
||||||
@@ -200,7 +201,8 @@ public class AihrModelSeedService {
|
|||||||
"AI 调用已被试点成本闸门关闭,当前返回文本兜底:先安抚业主情绪,确认事实与责任人,再承诺首次反馈时间。",
|
"AI 调用已被试点成本闸门关闭,当前返回文本兜底:先安抚业主情绪,确认事实与责任人,再承诺首次反馈时间。",
|
||||||
"cost-guard-fallback",
|
"cost-guard-fallback",
|
||||||
null,
|
null,
|
||||||
List.of("需要恢复真实调用时设置 AIHR_AI_RUNTIME_ENABLED=true 且 AIHR_AI_CHAT_ENABLED=true。")
|
List.of("需要恢复真实调用时设置 AIHR_AI_RUNTIME_ENABLED=true 且 AIHR_AI_CHAT_ENABLED=true。"),
|
||||||
|
null
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -215,13 +217,14 @@ public class AihrModelSeedService {
|
|||||||
List.of(
|
List.of(
|
||||||
"在 aihr_model_provider / aihr_model_config 配置 api_host、api_key、model_name 后可切换到真实 OpenAI-compatible 调用。",
|
"在 aihr_model_provider / aihr_model_config 配置 api_host、api_key、model_name 后可切换到真实 OpenAI-compatible 调用。",
|
||||||
"接口不会返回 api_key。"
|
"接口不会返回 api_key。"
|
||||||
)
|
),
|
||||||
|
null
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
String content = callOpenAiCompatible(runtime, modelName, prompt, request == null ? null : request.systemPrompt(), 0.2);
|
ModelCallResult call = callOpenAiCompatible(runtime, modelName, prompt, request == null ? null : request.systemPrompt(), 0.2);
|
||||||
return new ChatResponse(true, runtime.providerCode(), modelName, content, "openai-compatible", null, List.of());
|
return new ChatResponse(true, runtime.providerCode(), modelName, call.content(), "openai-compatible", null, List.of(), call.usage());
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
return new ChatResponse(
|
return new ChatResponse(
|
||||||
true,
|
true,
|
||||||
@@ -230,7 +233,8 @@ public class AihrModelSeedService {
|
|||||||
"真实模型调用失败,已回退内置默认兜底:先确认事实、同步时限、再生成工单闭环。",
|
"真实模型调用失败,已回退内置默认兜底:先确认事实、同步时限、再生成工单闭环。",
|
||||||
"openai-compatible-failed",
|
"openai-compatible-failed",
|
||||||
"模型调用失败,已回退内置兜底;请检查模型供应商配置。",
|
"模型调用失败,已回退内置兜底;请检查模型供应商配置。",
|
||||||
List.of("检查 aihr_model_provider.api_host/api_key 和 aihr_model_config.model_name 是否与供应商一致。")
|
List.of("检查 aihr_model_provider.api_host/api_key 和 aihr_model_config.model_name 是否与供应商一致。"),
|
||||||
|
null
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -284,7 +288,7 @@ public class AihrModelSeedService {
|
|||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
return Optional.of(callOpenAiCompatible(runtime, runtime.modelName(), userPrompt, systemPrompt, temperature));
|
return Optional.of(callOpenAiCompatible(runtime, runtime.modelName(), userPrompt, systemPrompt, temperature).content());
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warn("aihr llm tryChat failed, caller falls back to seed(处理错误已隐藏)");
|
log.warn("aihr llm tryChat failed, caller falls back to seed(处理错误已隐藏)");
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
@@ -376,7 +380,8 @@ public class AihrModelSeedService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private String callOpenAiCompatible(RuntimeConfig runtime, String modelName, String prompt, String systemPrompt, double temperature) throws Exception {
|
private ModelCallResult callOpenAiCompatible(RuntimeConfig runtime, String modelName, String prompt, String systemPrompt, double temperature) throws Exception {
|
||||||
|
long startedAt = System.nanoTime();
|
||||||
ObjectNode body = objectMapper.createObjectNode();
|
ObjectNode body = objectMapper.createObjectNode();
|
||||||
body.put("model", modelName);
|
body.put("model", modelName);
|
||||||
body.put("temperature", temperature);
|
body.put("temperature", temperature);
|
||||||
@@ -422,7 +427,21 @@ public class AihrModelSeedService {
|
|||||||
if (isBlank(content)) {
|
if (isBlank(content)) {
|
||||||
throw new IllegalStateException("LLM response missing message.content");
|
throw new IllegalStateException("LLM response missing message.content");
|
||||||
}
|
}
|
||||||
return content;
|
JsonNode usage = root.path("usage");
|
||||||
|
ModelUsage modelUsage = new ModelUsage(
|
||||||
|
integerUsage(usage, "prompt_tokens"),
|
||||||
|
integerUsage(usage, "completion_tokens"),
|
||||||
|
integerUsage(usage, "total_tokens"),
|
||||||
|
Duration.ofNanos(System.nanoTime() - startedAt).toMillis()
|
||||||
|
);
|
||||||
|
return new ModelCallResult(content, modelUsage);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Integer integerUsage(JsonNode usage, String field) {
|
||||||
|
if (usage == null || usage.isMissingNode() || !usage.has(field) || !usage.get(field).canConvertToInt()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return usage.get(field).intValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
private RuntimeConfig runtimeConfig(String requestedModel) {
|
private RuntimeConfig runtimeConfig(String requestedModel) {
|
||||||
@@ -593,6 +612,9 @@ public class AihrModelSeedService {
|
|||||||
private record RuntimeConfig(String providerCode, String modelName, String baseUrl, String apiKey, boolean configured, String source) {
|
private record RuntimeConfig(String providerCode, String modelName, String baseUrl, String apiKey, boolean configured, String source) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private record ModelCallResult(String content, ModelUsage usage) {
|
||||||
|
}
|
||||||
|
|
||||||
private record ProviderData(
|
private record ProviderData(
|
||||||
String providerName,
|
String providerName,
|
||||||
String providerCode,
|
String providerCode,
|
||||||
|
|||||||
+3
@@ -48,6 +48,9 @@ class AihrSensitiveTextTest {
|
|||||||
String speechControllerSource = Files.readString(controllerSource("AihrSpeechController.java"));
|
String speechControllerSource = Files.readString(controllerSource("AihrSpeechController.java"));
|
||||||
|
|
||||||
assertTrue(modelSource.contains("AihrSensitiveText.forModel(prompt)"));
|
assertTrue(modelSource.contains("AihrSensitiveText.forModel(prompt)"));
|
||||||
|
assertTrue(modelSource.contains("prompt_tokens"));
|
||||||
|
assertTrue(modelSource.contains("completion_tokens"));
|
||||||
|
assertTrue(modelSource.contains("latencyMs"));
|
||||||
assertTrue(modelSource.contains("外部响应体已隐藏"));
|
assertTrue(modelSource.contains("外部响应体已隐藏"));
|
||||||
assertTrue(modelSource.contains("模型调用失败,已回退内置兜底;请检查模型供应商配置。"));
|
assertTrue(modelSource.contains("模型调用失败,已回退内置兜底;请检查模型供应商配置。"));
|
||||||
assertTrue(sopSource.contains("AihrSensitiveText.forModel(renderPrompt(prompt.template(), queryText, context))"));
|
assertTrue(sopSource.contains("AihrSensitiveText.forModel(renderPrompt(prompt.template(), queryText, context))"));
|
||||||
|
|||||||
@@ -197,3 +197,4 @@
|
|||||||
- 2026-07-14 BRD 4.4 每日三题结果留痕补强:每日三题提交返回的分数此前只存在响应内,刷新任务列表后无法恢复数值分数;现新增 `aihr_practice_assignment.score`,提交、查询、员工学习页均保留“得分 + 反馈”。当前得分仍来自可解释关键词规则兜底,不把它宣称为语义级 AI 评分;后续若启用模型评估,需单独补评分模式、成本闸门和人工抽检证据。
|
- 2026-07-14 BRD 4.4 每日三题结果留痕补强:每日三题提交返回的分数此前只存在响应内,刷新任务列表后无法恢复数值分数;现新增 `aihr_practice_assignment.score`,提交、查询、员工学习页均保留“得分 + 反馈”。当前得分仍来自可解释关键词规则兜底,不把它宣称为语义级 AI 评分;后续若启用模型评估,需单独补评分模式、成本闸门和人工抽检证据。
|
||||||
- 2026-07-14 BRD 4.4 每日一练评分能力复核:当前 `scoreDailyDrill` 使用参考答案关键词命中和长度规则即时反馈,属于可解释的规则兜底,不等同于 BRD 所称的语义级“AI 秒评”;正式升级前需确认模型、成本闸门、评分维度和人工抽检样本,当前不把规则分数宣称为 AI 评测结果。
|
- 2026-07-14 BRD 4.4 每日一练评分能力复核:当前 `scoreDailyDrill` 使用参考答案关键词命中和长度规则即时反馈,属于可解释的规则兜底,不等同于 BRD 所称的语义级“AI 秒评”;正式升级前需确认模型、成本闸门、评分维度和人工抽检样本,当前不把规则分数宣称为 AI 评测结果。
|
||||||
- 2026-07-14 BRD G6 成本计量复核:模型调用已有手动运行断路器,但当前没有 `llm_call_log` 或等价的租户级 token、延迟、费用持久化;由于供应商计费字段、单价和月度阈值尚未确认,本轮不新增价格硬编码或生产数据表,先把该项保留为生产前闸门。
|
- 2026-07-14 BRD G6 成本计量复核:模型调用已有手动运行断路器,但当前没有 `llm_call_log` 或等价的租户级 token、延迟、费用持久化;由于供应商计费字段、单价和月度阈值尚未确认,本轮不新增价格硬编码或生产数据表,先把该项保留为生产前闸门。
|
||||||
|
- 2026-07-14 TechSpec 3.8 调用反馈补强:模型测试接口成功调用时现在返回供应商 `prompt_tokens/completion_tokens/total_tokens` 和本次 `latencyMs`;未配置、成本断路器或调用失败仍返回空 usage,不伪造 token 或费用。该反馈可作为后续 `llm_call_log` 的输入,但不等于已完成月度成本计量和自动阈值降级。
|
||||||
|
|||||||
@@ -62,6 +62,12 @@ export type ModelChatResponse = {
|
|||||||
mode: string;
|
mode: string;
|
||||||
error?: string;
|
error?: string;
|
||||||
hints: string[];
|
hints: string[];
|
||||||
|
usage?: {
|
||||||
|
promptTokens?: number;
|
||||||
|
completionTokens?: number;
|
||||||
|
totalTokens?: number;
|
||||||
|
latencyMs: number;
|
||||||
|
} | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type VectorIndexStatus = {
|
export type VectorIndexStatus = {
|
||||||
|
|||||||
Reference in New Issue
Block a user