feat: 新增max_tokens和调整

dev_na
puz 2026-07-16 11:02:00 +08:00
parent 8703b8bdc7
commit 3df2e753c5
11 changed files with 129 additions and 10 deletions

View File

@ -412,6 +412,7 @@ CREATE TABLE biz_llm_models (
model_code VARCHAR(100),
temperature DECIMAL(3,2) DEFAULT 0.7,
top_p DECIMAL(3,2) DEFAULT 0.9,
max_tokens BIGINT NOT NULL DEFAULT 30000,
is_default SMALLINT DEFAULT 0,
sort_order INTEGER NOT NULL DEFAULT 0,
status SMALLINT DEFAULT 1,

View File

@ -1,5 +1,6 @@
package com.imeeting.dto.biz;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
@ -32,6 +33,9 @@ public class AiModelDTO {
private BigDecimal temperature;
@Schema(description = "TopP 参数")
private BigDecimal topP;
@JsonProperty("max_tokens")
@Schema(description = "最大输出 token 数")
private Long maxTokens;
@Schema(description = "媒体配置")
private Map<String, Object> mediaConfig;
@Schema(description = "是否默认")

View File

@ -1,5 +1,6 @@
package com.imeeting.dto.biz;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
@ -33,6 +34,9 @@ public class AiModelVO {
private BigDecimal temperature;
@Schema(description = "TopP 参数")
private BigDecimal topP;
@JsonProperty("max_tokens")
@Schema(description = "最大输出 token 数")
private Long maxTokens;
@Schema(description = "多媒体配置")
private Map<String, Object> mediaConfig;
@Schema(description = "是否为默认模型")

View File

@ -1,6 +1,7 @@
package com.imeeting.entity.biz;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.unisbase.entity.BaseEntity;
@ -43,6 +44,10 @@ public class LlmModel extends BaseEntity {
@Schema(description = "Top P参数")
private BigDecimal topP;
@TableField("max_tokens")
@Schema(description = "最大输出 token 数")
private Long maxTokens;
@Schema(description = "是否默认模型")
private Integer isDefault;

View File

@ -55,6 +55,7 @@ public class AiModelServiceImpl implements AiModelService {
private static final String MEDIA_TENCENT_OFFLINE_MODEL_CODE = "tencentOfflineModelCode";
private static final String MEDIA_TENCENT_REALTIME_MODEL_CODE = "tencentRealtimeModelCode";
private static final int DEFAULT_SORT_ORDER = 0;
private static final long DEFAULT_LLM_MAX_TOKENS = 30000L;
private static final String DEFAULT_LLM_API_PATH = "/v1/chat/completions";
private static final String DEFAULT_ANTHROPIC_API_PATH = "/messages";
private static final String CONNECTIVITY_TEST_SYSTEM_PROMPT = """
@ -397,7 +398,7 @@ public class AiModelServiceImpl implements AiModelService {
Map<String, Object> body = new LinkedHashMap<>();
body.put("model", dto.getModelCode().trim());
body.put("stream", false);
body.put("max_tokens", 32);
body.put("max_tokens", resolveConnectivityMaxTokens(dto.getMaxTokens()));
if (dto.getTemperature() != null) {
body.put("temperature", dto.getTemperature());
}
@ -425,7 +426,7 @@ public class AiModelServiceImpl implements AiModelService {
Map<String, Object> body = new LinkedHashMap<>();
body.put("model", dto.getModelCode().trim());
body.put("system", CONNECTIVITY_TEST_SYSTEM_PROMPT);
body.put("max_tokens", 32);
body.put("max_tokens", resolveConnectivityMaxTokens(dto.getMaxTokens()));
body.put("messages", List.of(
Map.of("role", "user", "content", resolveTestMessage(dto.getTestMessage()))
));
@ -461,7 +462,7 @@ public class AiModelServiceImpl implements AiModelService {
"parts", List.of(Map.of("text", resolveTestMessage(dto.getTestMessage())))
)
));
if (dto.getTemperature() != null || dto.getTopP() != null) {
if (dto.getTemperature() != null || dto.getTopP() != null || dto.getMaxTokens() != null) {
Map<String, Object> generationConfig = new LinkedHashMap<>();
if (dto.getTemperature() != null) {
generationConfig.put("temperature", dto.getTemperature());
@ -469,6 +470,9 @@ public class AiModelServiceImpl implements AiModelService {
if (dto.getTopP() != null) {
generationConfig.put("topP", dto.getTopP());
}
if (dto.getMaxTokens() != null) {
generationConfig.put("maxOutputTokens", resolveConnectivityMaxTokens(dto.getMaxTokens()));
}
body.put("generationConfig", generationConfig);
}
@ -874,6 +878,11 @@ public class AiModelServiceImpl implements AiModelService {
if (Integer.valueOf(1).equals(dto.getIsDefault()) && !Integer.valueOf(1).equals(dto.getStatus())) {
throw new RuntimeException("默认模型必须为启用状态");
}
if (TYPE_LLM.equals(normalizeType(dto.getModelType()))
&& dto.getMaxTokens() != null
&& dto.getMaxTokens() <= 0) {
throw new RuntimeException("max_tokens 必须为正整数");
}
validateTencentAsrConfig(dto);
// if ("custom".equals(normalizeProvider(dto.getProvider()))) {
// if (TYPE_ASR.equals(normalizeType(dto.getModelType()))) {
@ -1178,12 +1187,21 @@ public class AiModelServiceImpl implements AiModelService {
entity.setModelCode(dto.getModelCode());
entity.setTemperature(dto.getTemperature() == null ? BigDecimal.valueOf(0.2) : dto.getTemperature());
entity.setTopP(dto.getTopP() == null ? BigDecimal.valueOf(0.9) : dto.getTopP());
entity.setMaxTokens(resolveMaxTokens(dto.getMaxTokens()));
entity.setIsDefault(dto.getIsDefault());
entity.setStatus(dto.getStatus());
entity.setSortOrder(normalizeSortOrder(dto.getSortOrder()));
entity.setRemark(dto.getRemark());
}
private Long resolveMaxTokens(Long maxTokens) {
return maxTokens == null ? DEFAULT_LLM_MAX_TOKENS : maxTokens;
}
private Long resolveConnectivityMaxTokens(Long maxTokens) {
return maxTokens == null ? 32L : maxTokens;
}
private AiModelVO toAsrVO(AsrModel entity) {
return toAsrVO(entity, null, true);
}
@ -1281,6 +1299,7 @@ public class AiModelServiceImpl implements AiModelService {
vo.setModelCode(entity.getModelCode());
vo.setTemperature(entity.getTemperature());
vo.setTopP(entity.getTopP());
vo.setMaxTokens(resolveMaxTokens(entity.getMaxTokens()));
vo.setIsDefault(entity.getIsDefault());
vo.setStatus(entity.getStatus());
vo.setScope(Long.valueOf(0L).equals(entity.getTenantId()) ? "PLATFORM" : "TENANT");

View File

@ -1325,7 +1325,7 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
Map<String, Object> req = new HashMap<>();
req.put("model", llmModel.getModelCode());
req.put("temperature", llmModel.getTemperature());
req.put("max_tokens", 30000);
req.put("max_tokens", llmModel.getMaxTokens() == null ? 30000L : llmModel.getMaxTokens());
req.put("messages", List.of(
Map.of("role", "system", "content", meetingSummaryPromptAssembler.buildSystemMessage(taskRecord.getTaskConfig())),
Map.of("role", "user", "content", meetingSummaryPromptAssembler.buildUserMessage(taskRecord.getTaskConfig(), meeting, summarySource, userPrompt))

View File

@ -200,6 +200,7 @@ public class MeetingExternalSummaryWebhookTrigger {
config.put("modelCode", model.getModelCode());
config.put("temperature", model.getTemperature());
config.put("topP", model.getTopP());
config.put("max_tokens", model.getMaxTokens() == null ? 30000L : model.getMaxTokens());
return config;
}

View File

@ -335,7 +335,7 @@ public class MeetingTranscriptChapterServiceImpl implements MeetingTranscriptCha
Map<String, Object> requestBody = new LinkedHashMap<>();
requestBody.put("model", llmModel.getModelCode());
requestBody.put("temperature", llmModel.getTemperature());
requestBody.put("max_tokens", 30000);
requestBody.put("max_tokens", llmModel.getMaxTokens() == null ? 30000L : llmModel.getMaxTokens());
requestBody.put("messages", List.of(
Map.of("role", "system", "content", renderChapterSystemPrompt()),
Map.of("role", "user", "content", renderChapterUserPrompt(transcripts))
@ -1015,7 +1015,7 @@ public class MeetingTranscriptChapterServiceImpl implements MeetingTranscriptCha
Map<String, Object> requestBody = new LinkedHashMap<>();
requestBody.put("model", llmModel.getModelCode());
requestBody.put("temperature", llmModel.getTemperature());
requestBody.put("max_tokens", 30000);
requestBody.put("max_tokens", llmModel.getMaxTokens() == null ? 30000L : llmModel.getMaxTokens());
requestBody.put("messages", List.of(
Map.of("role", "system", "content", renderChapterSystemPrompt()),
Map.of("role", "user", "content", renderChapterUserPrompt(transcripts))

View File

@ -481,6 +481,64 @@ class AiModelServiceImplTest {
assertEquals(7, captor.getValue().getSortOrder());
}
@Test
void saveModelShouldPersistMaxTokensForLlm() throws Exception {
AsrModelMapper asrModelMapper = mock(AsrModelMapper.class);
LlmModelMapper llmModelMapper = mock(LlmModelMapper.class);
when(llmModelMapper.insert(any(LlmModel.class))).thenReturn(1);
AiModelServiceImpl service = new AiModelServiceImpl(
objectMapper,
asrModelMapper,
llmModelMapper
);
AiModelDTO dto = new AiModelDTO();
dto.setModelType("LLM");
dto.setModelName("max-token-llm");
dto.setProvider("openai");
dto.setBaseUrl("http://127.0.0.1:9000");
dto.setApiPath("/v1/chat/completions");
dto.setModelCode("gpt-test");
dto.setMaxTokens(8192L);
dto.setIsDefault(0);
dto.setStatus(1);
AiModelVO result = service.saveModel(dto);
ArgumentCaptor<LlmModel> captor = ArgumentCaptor.forClass(LlmModel.class);
verify(llmModelMapper, times(1)).insert(captor.capture());
assertEquals(8192L, captor.getValue().getMaxTokens());
assertEquals(8192L, result.getMaxTokens());
JsonNode json = objectMapper.readTree(objectMapper.writeValueAsString(result));
assertEquals(8192L, json.path("max_tokens").asLong());
assertEquals(false, json.has("maxTokens"));
}
@Test
void saveModelShouldRejectNonPositiveMaxTokensForLlm() {
AiModelServiceImpl service = new AiModelServiceImpl(
objectMapper,
mock(AsrModelMapper.class),
mock(LlmModelMapper.class)
);
AiModelDTO dto = new AiModelDTO();
dto.setModelType("LLM");
dto.setModelName("invalid-max-token-llm");
dto.setProvider("openai");
dto.setBaseUrl("http://127.0.0.1:9000");
dto.setApiPath("/v1/chat/completions");
dto.setModelCode("gpt-test");
dto.setMaxTokens(0L);
dto.setIsDefault(0);
dto.setStatus(1);
RuntimeException ex = assertThrows(RuntimeException.class, () -> service.saveModel(dto));
assertEquals("max_tokens 必须为正整数", ex.getMessage());
}
@Test
void saveModelShouldRejectDisabledDefaultModel() {
AiModelServiceImpl service = new AiModelServiceImpl(

View File

@ -13,6 +13,7 @@ export interface AiModelVO {
wsUrl?: string;
temperature?: number;
topP?: number;
max_tokens?: number;
mediaConfig?: Record<string, any>;
isDefault: number;
status: number;
@ -47,6 +48,7 @@ export interface AiModelDTO {
wsUrl?: string;
temperature?: number;
topP?: number;
max_tokens?: number;
mediaConfig?: Record<string, any>;
isDefault: number;
status: number;
@ -157,6 +159,7 @@ export const testLlmModelConnectivity = (data: {
modelCode: string;
temperature?: number;
topP?: number;
max_tokens?: number;
testMessage: string;
}) => {
return http.post<{ code: string; data: boolean; msg: string }>(

View File

@ -323,6 +323,7 @@ const AiModels: React.FC = () => {
: undefined,
temperature: values.temperature,
topP: values.topP,
max_tokens: values.max_tokens,
isDefault: values.isDefaultChecked ? 1 : 0,
status: values.statusChecked ? 1 : 0,
sortOrder: values.sortOrder ?? 0,
@ -347,8 +348,8 @@ const AiModels: React.FC = () => {
const handleTestConnectivity = async () => {
if (activeType === "LLM") {
const values = await form.validateFields(["provider", "baseUrl", "apiPath", "modelCode"]);
const extraValues = form.getFieldsValue(["apiKey", "temperature", "topP"]);
const values = await form.validateFields(["provider", "baseUrl", "apiPath", "modelCode", "max_tokens"]);
const extraValues = form.getFieldsValue(["apiKey", "temperature", "topP", "max_tokens"]);
setConnectivityLoading(true);
try {
await testLlmModelConnectivity({
@ -359,6 +360,7 @@ const AiModels: React.FC = () => {
modelCode: values.modelCode,
temperature: extraValues.temperature,
topP: extraValues.topP,
max_tokens: extraValues.max_tokens,
testMessage: DEFAULT_LLM_TEST_MESSAGE,
});
message.success("LLM 连通性测试成功");
@ -773,16 +775,38 @@ const AiModels: React.FC = () => {
<Input />
</Form.Item>
<Row gutter={16} className="app-responsive-form-row">
<Col xs={24} md={12}>
<Col xs={24} md={8}>
<Form.Item name="temperature" label="Temperature">
<InputNumber min={0} max={2} step={0.1} style={{ width: "100%" }} />
</Form.Item>
</Col>
<Col xs={24} md={12}>
<Col xs={24} md={8}>
<Form.Item name="topP" label="Top P">
<InputNumber min={0} max={1} step={0.1} style={{ width: "100%" }} />
</Form.Item>
</Col>
<Col xs={24} md={8}>
<Form.Item
name="max_tokens"
label="max_tokens"
rules={[
{ required: true, message: "请输入 max_tokens" },
{
validator: (_, value) => {
if (value === undefined || value === null || value === "") {
return Promise.resolve();
}
if (Number.isInteger(value) && value > 0) {
return Promise.resolve();
}
return Promise.reject(new Error("max_tokens 必须为正整数"));
},
},
]}
>
<InputNumber min={1} precision={0} style={{ width: "100%" }} />
</Form.Item>
</Col>
</Row>
</>
)}