feat: 增强智能体模型调用兼容能力

- 增加模型流式开关和 HTTP 传输策略配置

- 使用 AgentScope 执行基础连接、流式与 VLM 双阶段验证

- 固定多模态校验图片并统一验证状态展示
This commit is contained in:
2026-07-17 19:57:06 +08:00
parent ba21f861f4
commit 791649c7d5
22 changed files with 1492 additions and 107 deletions

View File

@@ -37,6 +37,21 @@ export async function verifyModelConfig(id: string) {
return api.get('/api/v1/model/verifyLlmConfig', { params: { id } });
}
export type ModelVerificationStageStatus =
| 'FAILED'
| 'PARTIAL'
| 'PASSED'
| 'SKIPPED';
export interface ModelVerificationData {
dimension?: number;
effectiveHttpVersion?: string;
message?: string;
nonStreaming?: ModelVerificationStageStatus;
status?: ModelVerificationStageStatus;
streaming?: ModelVerificationStageStatus;
}
export interface ModelInvokeConfigPayload {
id: string;
invokeCode?: string;

View File

@@ -1,8 +1,8 @@
<script setup lang="ts">
/* eslint-disable vue/no-mutating-props */
import type {AgentInfo, AgentOption} from '../types';
import type { AgentInfo, AgentOption } from '../types';
import {InfoFilled} from '@element-plus/icons-vue';
import { InfoFilled } from '@element-plus/icons-vue';
import {
ElForm,
ElFormItem,
@@ -11,6 +11,7 @@ import {
ElInputNumber,
ElOption,
ElSelect,
ElSwitch,
ElTooltip,
} from 'element-plus';
@@ -75,6 +76,27 @@ const emit = defineEmits<{ change: [] }>();
/>
</ElSelect>
</ElFormItem>
<ElFormItem>
<template #label>
<span class="agent-form__label">
模型流式响应
<ElTooltip
content="关闭后,模型会在生成完成后一次性返回回答。"
effect="light"
placement="top"
>
<ElIcon class="agent-form__info" aria-label="模型流式响应说明">
<InfoFilled />
</ElIcon>
</ElTooltip>
</span>
</template>
<ElSwitch
v-model="agent.generationConfigJson!.stream"
aria-label="模型流式响应"
@change="emit('change')"
/>
</ElFormItem>
<ElFormItem label="系统提示词">
<ElInput
v-model="agent.promptConfigJson!.systemPrompt"

View File

@@ -0,0 +1,33 @@
import { describe, expect, it } from 'vitest';
import {
createEmptyAgent,
useAgentDesignerState,
} from './useAgentDesignerState';
describe('useAgentDesignerState generation stream', () => {
it('defaults new and legacy agents to streaming', () => {
expect(createEmptyAgent().generationConfigJson?.stream).toBe(true);
const designer = useAgentDesignerState();
designer.reset({ name: '旧智能体' });
expect(designer.state.agent.generationConfigJson?.stream).toBe(true);
expect(designer.buildPayloadAgent().generationConfigJson?.stream).toBe(
true,
);
});
it('preserves an explicitly disabled stream value in the payload', () => {
const designer = useAgentDesignerState();
designer.reset({
name: '非流式智能体',
generationConfigJson: { stream: false },
});
expect(designer.state.agent.generationConfigJson?.stream).toBe(false);
expect(designer.buildPayloadAgent().generationConfigJson?.stream).toBe(
false,
);
});
});

View File

@@ -84,6 +84,7 @@ export function createEmptyAgent(): AgentInfo {
categoryId: '',
modelId: '',
promptConfigJson: { systemPrompt: '' },
generationConfigJson: { stream: true },
memoryConfigJson: {
compressionParameter: {
enabled: true,
@@ -111,6 +112,10 @@ function normalizeAgent(agent?: AgentInfo): AgentInfo {
systemPrompt: '',
...source.promptConfigJson,
},
generationConfigJson: {
...source.generationConfigJson,
stream: source.generationConfigJson?.stream !== false,
},
memoryConfigJson: {
...memoryConfig,
compressionParameter: {
@@ -356,6 +361,10 @@ export function useAgentDesignerState() {
interactionConfigJson: buildInteractionConfigPayload(
state.agent.interactionConfigJson,
),
generationConfigJson: {
...state.agent.generationConfigJson,
stream: state.agent.generationConfigJson?.stream !== false,
},
memoryConfigJson: {
...restMemoryConfigJson,
compressionParameter: {

View File

@@ -1,6 +1,7 @@
<script setup lang="ts">
import type { llmType } from '#/api';
import type { ModelAbilityItem } from '#/views/ai/model/modelUtils/model-ability';
import type { VerifyButtonStatus } from '#/views/ai/model/modelUtils/model-verification';
import { computed, onMounted, reactive, ref } from 'vue';
@@ -11,6 +12,7 @@ import {
Edit,
Loading,
Select,
Warning,
} from '@element-plus/icons-vue';
import {
ElButton,
@@ -31,6 +33,10 @@ import { $t } from '#/locales';
import ModelProviderBadge from '#/views/ai/model/ModelProviderBadge.vue';
import { getDefaultModelAbility } from '#/views/ai/model/modelUtils/model-ability';
import { mapLlmToModelAbility } from '#/views/ai/model/modelUtils/model-ability-utils';
import {
getVerifyButtonText as getStatusButtonText,
resolveModelVerificationFeedback,
} from '#/views/ai/model/modelUtils/model-verification';
interface ProviderOption {
id: string;
@@ -63,7 +69,6 @@ const isActionLoading = ref(false);
const modelRows = ref<llmType[]>([]);
const selectedRows = ref<llmType[]>([]);
const lastErrorMessage = ref('');
type VerifyButtonStatus = 'error' | 'idle' | 'loading' | 'success';
const verifyStatusMap = ref<Record<string, VerifyButtonStatus>>({});
const filterState = reactive<FilterState>({
@@ -121,17 +126,7 @@ const setVerifyStatus = (id: string, status: VerifyButtonStatus) => {
};
const isVerifying = (row: llmType) => getVerifyStatus(row) === 'loading';
const getVerifyButtonText = (row: llmType) => {
const status = getVerifyStatus(row);
if (status === 'loading') {
return '验证中';
}
if (status === 'success') {
return '验证成功';
}
if (status === 'error') {
return '验证失败';
}
return '验证配置';
return getStatusButtonText(getVerifyStatus(row));
};
const getVerifyButtonIcon = (row: llmType) => {
const status = getVerifyStatus(row);
@@ -141,6 +136,9 @@ const getVerifyButtonIcon = (row: llmType) => {
if (status === 'success') {
return CircleCheck;
}
if (status === 'warning') {
return Warning;
}
if (status === 'error') {
return CircleClose;
}
@@ -285,17 +283,16 @@ const handleVerify = async (row: llmType) => {
try {
const res = await verifyModelConfig(modelId);
if (res.errorCode === 0) {
setVerifyStatus(modelId, 'success');
if (row.modelType === 'embeddingModel' && res?.data?.dimension) {
ElMessage.success(`验证成功,向量维度:${res.data.dimension}`);
} else {
ElMessage.success('验证成功');
}
const feedback = resolveModelVerificationFeedback(res, row.modelType);
setVerifyStatus(modelId, feedback.status);
if (feedback.status === 'success') {
ElMessage.success(feedback.message);
} else if (feedback.status === 'warning') {
ElMessage.warning(feedback.message);
} else {
setVerifyStatus(modelId, 'error');
if (!res.message) {
ElMessage.error($t('ui.actionMessage.operationFailed'));
ElMessage.error(feedback.message);
}
}
} catch {
@@ -717,6 +714,10 @@ defineExpose({
color: hsl(var(--success));
}
.active-workspace__verify-btn.is-warning {
color: hsl(var(--warning));
}
.active-workspace__verify-btn.is-error {
color: hsl(var(--destructive));
}
@@ -732,6 +733,7 @@ defineExpose({
}
.active-workspace__verify-icon.is-success,
.active-workspace__verify-icon.is-warning,
.active-workspace__verify-icon.is-error {
animation: active-workspace-verify-pop 0.32s ease;
}

View File

@@ -6,7 +6,14 @@ import { computed, reactive, ref, watch } from 'vue';
import { EasyFlowFormModal } from '@easyflow/common-ui';
import { IconifyIcon } from '@easyflow/icons';
import { ElForm, ElFormItem, ElInput, ElMessage } from 'element-plus';
import {
ElForm,
ElFormItem,
ElInput,
ElMessage,
ElOption,
ElSelect,
} from 'element-plus';
import { api } from '#/api/request';
import { $t } from '#/locales';
@@ -19,6 +26,13 @@ import {
resetModelAbility,
} from '#/views/ai/model/modelUtils/model-ability-utils';
type AgentHttpVersionPolicy = 'AUTO' | 'HTTP_1_1' | 'HTTP_2_PREFERRED';
interface ModelOptions {
agentHttpVersionPolicy: AgentHttpVersionPolicy;
[key: string]: unknown;
}
interface FormData {
id?: string;
modelType: string;
@@ -39,6 +53,7 @@ interface FormData {
supportVideo: boolean;
supportImageB64Only: boolean;
supportToolMessage: boolean;
options: ModelOptions;
}
const props = defineProps({
@@ -85,8 +100,39 @@ const formData = reactive<FormData>({
supportVideo: false,
supportImageB64Only: false,
supportToolMessage: true,
options: {
agentHttpVersionPolicy: 'AUTO',
},
});
const agentHttpVersionOptions = [
{ label: '自动', value: 'AUTO' },
{ label: 'HTTP/1.1 兼容', value: 'HTTP_1_1' },
{ label: 'HTTP/2 优先', value: 'HTTP_2_PREFERRED' },
] as const;
const normalizeAgentHttpVersionPolicy = (
value?: unknown,
): AgentHttpVersionPolicy => {
if (value === 'HTTP_1_1' || value === 'HTTP_2_PREFERRED') {
return value;
}
return 'AUTO';
};
const normalizeModelOptions = (options?: unknown): ModelOptions => {
const source =
options && typeof options === 'object' && !Array.isArray(options)
? (options as Record<string, unknown>)
: {};
return {
...source,
agentHttpVersionPolicy: normalizeAgentHttpVersionPolicy(
source.agentHttpVersionPolicy,
),
};
};
const modelAbility = ref<ModelAbilityItem[]>(getDefaultModelAbility());
const visibleModelAbility = computed(() =>
modelAbility.value.filter(
@@ -189,6 +235,7 @@ const resetFormData = () => {
supportImageB64Only: false,
supportFree: false,
supportToolMessage: true,
options: normalizeModelOptions(),
});
};
@@ -231,6 +278,7 @@ defineExpose({
supportFree: item.supportFree || false,
supportToolMessage:
item.supportToolMessage === undefined ? true : item.supportToolMessage,
options: normalizeModelOptions(item.options),
});
selectedModelType.value = normalizeSelectableModelType(item.modelType);
if (selectedModelType.value) {
@@ -386,6 +434,20 @@ const save = async () => {
</div>
</div>
</ElFormItem>
<ElFormItem v-if="!hasSpecialModelType" label="Agent HTTP 传输">
<ElSelect
v-model="formData.options.agentHttpVersionPolicy"
aria-label="Agent HTTP 传输"
>
<ElOption
v-for="item in agentHttpVersionOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</ElSelect>
</ElFormItem>
</div>
</ElForm>
</div>

View File

@@ -12,10 +12,11 @@ import {
ElSelect,
} from 'element-plus';
import { api } from '#/api/request';
import { getModelList, verifyModelConfig } from '#/api/ai/llm';
import { $t } from '#/locales';
import { resolveModelVerificationFeedback } from '#/views/ai/model/modelUtils/model-verification';
type VerifyStatus = 'error' | 'idle' | 'success';
type VerifyStatus = 'error' | 'idle' | 'success' | 'warning';
const options = ref<any[]>([]);
const modelType = ref('');
@@ -39,11 +40,15 @@ const resultTitle = computed(() => {
return '验证失败';
}
if (verifyStatus.value === 'warning') {
return '流式不可用';
}
return '等待验证';
});
const getLlmList = async (providerId: string) => {
const res = await api.get(`/api/v1/model/list?providerId=${providerId}`, {});
const res = await getModelList({ providerId });
if (res.errorCode === 0) {
options.value = res.data;
}
@@ -87,17 +92,21 @@ const save = async () => {
try {
await formDataRef.value.validate();
const res = await api.get(
`/api/v1/model/verifyLlmConfig?id=${formData.llmId}`,
{},
);
const res = await verifyModelConfig(formData.llmId);
if (res.errorCode === 0) {
verifyStatus.value = 'success';
verifyMessage.value = $t('llm.testSuccess');
ElMessage.success($t('llm.testSuccess'));
if (modelType.value === 'embeddingModel' && res?.data?.dimension) {
vectorDimension.value = res.data.dimension;
const feedback = resolveModelVerificationFeedback(res, modelType.value);
verifyStatus.value = feedback.status;
verifyMessage.value = feedback.message;
if (feedback.status === 'warning') {
ElMessage.warning(feedback.message);
} else if (feedback.status === 'success') {
ElMessage.success(feedback.message);
} else {
ElMessage.error(feedback.message);
}
if (modelType.value === 'embeddingModel' && feedback.dimension) {
vectorDimension.value = String(feedback.dimension);
}
} else {
verifyStatus.value = 'error';
@@ -136,9 +145,7 @@ const save = async () => {
<section class="verify-modal__section">
<div class="verify-modal__section-head">
<h3>1. 选择待验证模型</h3>
<p>
会用当前保存的服务商配置发起一次真实请求帮助你确认密钥和路径是否正确
</p>
<p>会用当前保存的配置检查基础连接和流式响应</p>
</div>
<ElForm
@@ -170,13 +177,14 @@ const save = async () => {
<section class="verify-modal__section verify-modal__section--result">
<div class="verify-modal__section-head">
<h3>2. 查看验证结果</h3>
<p>成功后会返回可用状态如果是向量模型还会展示向量维度</p>
<p>会展示可用状态向量模型还会展示向量维度</p>
</div>
<div
class="verify-result-card"
:class="{
'is-success': verifyStatus === 'success',
'is-warning': verifyStatus === 'warning',
'is-error': verifyStatus === 'error',
}"
>
@@ -253,6 +261,11 @@ const save = async () => {
border-color: hsl(var(--success) / 36%);
}
.verify-result-card.is-warning {
background: hsl(var(--warning) / 6%);
border-color: hsl(var(--warning) / 36%);
}
.verify-result-card.is-error {
background: hsl(var(--destructive) / 5%);
border-color: hsl(var(--destructive) / 36%);

View File

@@ -3,6 +3,7 @@ import type { PropType } from 'vue';
import type { llmType } from '#/api';
import type { ModelAbilityItem } from '#/views/ai/model/modelUtils/model-ability';
import type { VerifyButtonStatus } from '#/views/ai/model/modelUtils/model-verification';
import { ref } from 'vue';
@@ -13,6 +14,7 @@ import {
Edit,
Loading,
Select,
Warning,
} from '@element-plus/icons-vue';
import { ElButton, ElIcon, ElMessage, ElTag } from 'element-plus';
@@ -20,6 +22,10 @@ import { verifyModelConfig } from '#/api/ai/llm';
import ModelProviderBadge from '#/views/ai/model/ModelProviderBadge.vue';
import { getDefaultModelAbility } from '#/views/ai/model/modelUtils/model-ability';
import { mapLlmToModelAbility } from '#/views/ai/model/modelUtils/model-ability-utils';
import {
getVerifyButtonText as getStatusButtonText,
resolveModelVerificationFeedback,
} from '#/views/ai/model/modelUtils/model-verification';
const props = defineProps({
llmList: {
@@ -33,7 +39,6 @@ const props = defineProps({
});
const emit = defineEmits(['deleteLlm', 'editLlm']);
type VerifyButtonStatus = 'error' | 'idle' | 'loading' | 'success';
const verifyStatusMap = ref<Record<string, VerifyButtonStatus>>({});
const getModelId = (llm: llmType) =>
String((llm as any).id || (llm as any).llmId || (llm as any).modelId || '');
@@ -47,17 +52,7 @@ const setVerifyStatus = (id: string, status: VerifyButtonStatus) => {
verifyStatusMap.value[id] = status;
};
const getVerifyButtonText = (llm: llmType) => {
const status = getVerifyStatus(llm);
if (status === 'loading') {
return '验证中';
}
if (status === 'success') {
return '验证成功';
}
if (status === 'error') {
return '验证失败';
}
return '验证配置';
return getStatusButtonText(getVerifyStatus(llm));
};
const getVerifyButtonIcon = (llm: llmType) => {
const status = getVerifyStatus(llm);
@@ -67,6 +62,9 @@ const getVerifyButtonIcon = (llm: llmType) => {
if (status === 'success') {
return CircleCheck;
}
if (status === 'warning') {
return Warning;
}
if (status === 'error') {
return CircleClose;
}
@@ -97,17 +95,16 @@ const handleVerifyLlm = async (llm: llmType) => {
try {
const res = await verifyModelConfig(modelId);
if (res.errorCode === 0) {
setVerifyStatus(modelId, 'success');
if (llm.modelType === 'embeddingModel' && res?.data?.dimension) {
ElMessage.success(`验证成功,向量维度:${res.data.dimension}`);
} else {
ElMessage.success('验证成功');
}
const feedback = resolveModelVerificationFeedback(res, llm.modelType);
setVerifyStatus(modelId, feedback.status);
if (feedback.status === 'success') {
ElMessage.success(feedback.message);
} else if (feedback.status === 'warning') {
ElMessage.warning(feedback.message);
} else {
setVerifyStatus(modelId, 'error');
if (!res.message) {
ElMessage.error('验证失败');
ElMessage.error(feedback.message);
}
}
} catch {
@@ -309,6 +306,10 @@ const getSelectedAbilityTagsForLlm = (llm: llmType): ModelAbilityItem[] => {
color: hsl(var(--success));
}
.llm-item__verify-btn.is-warning {
color: hsl(var(--warning));
}
.llm-item__verify-btn.is-error {
color: hsl(var(--destructive));
}
@@ -324,6 +325,7 @@ const getSelectedAbilityTagsForLlm = (llm: llmType): ModelAbilityItem[] => {
}
.llm-item__verify-icon.is-success,
.llm-item__verify-icon.is-warning,
.llm-item__verify-icon.is-error {
animation: llm-item-verify-pop 0.32s ease;
}

View File

@@ -0,0 +1,63 @@
import { describe, expect, it } from 'vitest';
import {
getVerifyButtonText,
resolveModelVerificationFeedback,
} from '../model-verification';
describe('model verification helpers', () => {
it('双阶段通过时返回成功状态', () => {
expect(
resolveModelVerificationFeedback(
{
data: { status: 'PASSED' },
errorCode: 0,
},
'chatModel',
),
).toEqual({
dimension: undefined,
message: '验证成功',
status: 'success',
});
});
it('基础连接通过但流式失败时返回警告状态', () => {
const feedback = resolveModelVerificationFeedback(
{
data: {
message: '连接成功,流式响应不可用,可关闭智能体的模型流式响应。',
status: 'PARTIAL',
},
errorCode: 0,
},
'chatModel',
);
expect(feedback.status).toBe('warning');
expect(feedback.message).toContain('流式响应不可用');
expect(getVerifyButtonText('warning')).toBe('流式不可用');
});
it('向量模型验证保留维度结果', () => {
expect(
resolveModelVerificationFeedback(
{ data: { dimension: 1024 }, errorCode: 0 },
'embeddingModel',
),
).toEqual({
dimension: 1024,
message: '验证成功向量维度1024',
status: 'success',
});
});
it('接口失败时返回错误状态', () => {
expect(
resolveModelVerificationFeedback(
{ errorCode: 1, message: '密钥无效' },
'chatModel',
),
).toEqual({ message: '密钥无效', status: 'error' });
});
});

View File

@@ -0,0 +1,75 @@
import type { ModelVerificationData } from '#/api/ai/llm';
export type VerifyButtonStatus =
| 'error'
| 'idle'
| 'loading'
| 'success'
| 'warning';
export interface ModelVerificationFeedback {
dimension?: number;
message: string;
status: 'error' | 'success' | 'warning';
}
interface ModelVerificationResponse {
data?: ModelVerificationData;
errorCode: number;
message?: string;
}
const STREAMING_UNAVAILABLE_MESSAGE =
'连接成功,流式响应不可用,可关闭智能体的模型流式响应。';
export function resolveModelVerificationFeedback(
response: ModelVerificationResponse,
modelType: string,
): ModelVerificationFeedback {
if (response.errorCode !== 0) {
return {
message: response.message || '验证失败',
status: 'error',
};
}
if (response.data?.status === 'PARTIAL') {
return {
message: response.data.message || STREAMING_UNAVAILABLE_MESSAGE,
status: 'warning',
};
}
if (response.data?.status === 'FAILED') {
return {
message: response.data.message || '验证失败',
status: 'error',
};
}
const dimension = response.data?.dimension;
return {
dimension,
message:
modelType === 'embeddingModel' && dimension
? `验证成功,向量维度:${dimension}`
: response.data?.message || '验证成功',
status: 'success',
};
}
export function getVerifyButtonText(status: VerifyButtonStatus): string {
if (status === 'loading') {
return '验证中';
}
if (status === 'success') {
return '验证成功';
}
if (status === 'warning') {
return '流式不可用';
}
if (status === 'error') {
return '验证失败';
}
return '验证配置';
}