feat(agent): control reasoning visibility

This commit is contained in:
2026-07-17 14:05:54 +08:00
parent bfaf2ebf67
commit a879dc2ff1
18 changed files with 368 additions and 51 deletions

View File

@@ -30,7 +30,13 @@ const historyDetailOpen = ref(false);
const selectedHistory = ref<PromptDetail | null>(null);
const historyDetailLoading = ref(false);
const agentDebugTrace = ref<Record<string, any>[]>([]);
const agentPreviewMessages = ref<{ role: "user" | "assistant" | "system"; content: string; streaming?: boolean }[]>([
const agentPreviewMessages = ref<{
role: "user" | "assistant" | "system";
content: string;
reasoning?: string;
showReasoning?: boolean;
streaming?: boolean;
}[]>([
{ role: "assistant", content: "选择模型和知识库后,可以在这里调试 Agent 的真实问答效果。" },
]);
@@ -54,6 +60,7 @@ const runtimeForm = reactive({
frequencyPenalty: null as number | null,
maxToken: 8192 as number | null,
streamEnabled: 1,
reasoningVisible: 0,
});
const promptDirty = computed(() => promptContent.value !== savedPromptContent.value);
@@ -109,6 +116,7 @@ function applyRuntimeConfig(value: AgentRuntimeConfig) {
frequencyPenalty: value.frequencyPenalty,
maxToken: value.maxToken,
streamEnabled: value.streamEnabled,
reasoningVisible: value.reasoningVisible,
});
}
@@ -138,6 +146,7 @@ async function saveRuntimeConfig() {
frequencyPenalty: runtimeForm.frequencyPenalty,
maxToken: runtimeForm.maxToken,
streamEnabled: runtimeForm.streamEnabled,
reasoningVisible: runtimeForm.reasoningVisible,
});
applyRuntimeConfig(saved);
const model = models.value.find((item) => item.id === saved.modelId);
@@ -289,6 +298,14 @@ async function debugAgent() {
currentAssistant().content += chunk;
scrollAgentPreview();
},
(chunk) => {
currentAssistant().reasoning = (currentAssistant().reasoning || "") + chunk;
scrollAgentPreview();
},
(status) => {
currentAssistant().showReasoning = status.reasoningVisible;
scrollAgentPreview();
},
(result) => {
agentDebugTrace.value = result.retrievalTrace || [];
ElMessage.success(result.message || "Agent 调试完成");
@@ -410,6 +427,21 @@ function errorMessage(error: unknown, fallback: string) {
:disabled="!runtimeConfig?.modelId"
/>
</el-form-item>
<el-form-item class="agent-stream-setting">
<div class="agent-stream-setting-copy">
<strong>展示思考过程</strong>
<span>开启后用户端和后台调试预览可展开查看模型返回的思考内容关闭时后端不会下发该内容</span>
</div>
<el-switch
v-model="runtimeForm.reasoningVisible"
:active-value="1"
:inactive-value="0"
active-text="开启"
inactive-text="关闭"
inline-prompt
:disabled="!runtimeConfig?.modelId"
/>
</el-form-item>
</section>
</el-form>
</el-tab-pane>
@@ -475,7 +507,12 @@ function errorMessage(error: unknown, fallback: string) {
<div class="agent-preview-bubble">
<strong>{{ message.role === "user" ? "你" : "Agent" }}</strong>
<template v-if="message.role === 'assistant'">
<StreamingMarkdownMessage :content="message.content" :streaming="message.streaming" />
<StreamingMarkdownMessage
:content="message.content"
:reasoning="message.reasoning"
:show-reasoning="message.showReasoning"
:streaming="message.streaming"
/>
</template>
<pre v-else>{{ message.content }}</pre>
</div>

View File

@@ -4,47 +4,60 @@ import { computed } from "vue";
const props = defineProps<{
content: string;
reasoning?: string;
showReasoning?: boolean;
streaming?: boolean;
}>();
const markdown = new MarkdownIt({ breaks: true, html: false, linkify: true });
const answer = computed(() => stripStreamingReasoning(props.content).replace(/^\s+/, ""));
const parsed = computed(() => splitReasoning(props.content));
const answer = computed(() => parsed.value.answer.replace(/^\s+/, ""));
const reasoning = computed(() => props.reasoning || parsed.value.reasoning);
const renderedContent = computed(() => answer.value.trim() ? markdown.render(answer.value) : "");
function stripStreamingReasoning(content: string) {
function splitReasoning(content: string) {
const lower = content.toLowerCase();
let visible = "";
let answer = "";
let reasoning = "";
let cursor = 0;
let reasoningDepth = 0;
let depth = 0;
while (cursor < content.length) {
const tagStart = content.indexOf("<", cursor);
if (tagStart === -1) {
if (reasoningDepth === 0) visible += content.slice(cursor);
if (depth) reasoning += content.slice(cursor);
else answer += content.slice(cursor);
break;
}
if (reasoningDepth === 0) visible += content.slice(cursor, tagStart);
const text = content.slice(cursor, tagStart);
if (depth) reasoning += text;
else answer += text;
const tail = lower.slice(tagStart);
const openTag = tail.match(/^<think(?:\s[^>]*)?>/);
if (openTag) {
reasoningDepth += 1;
depth += 1;
cursor = tagStart + openTag[0].length;
continue;
}
const closeTag = tail.match(/^<\/think\s*>/);
if (closeTag) {
reasoningDepth = Math.max(0, reasoningDepth - 1);
depth = Math.max(0, depth - 1);
cursor = tagStart + closeTag[0].length;
continue;
}
if ("<think".startsWith(tail) || "</think>".startsWith(tail)) break;
if (reasoningDepth === 0) visible += "<";
if (depth) reasoning += "<";
else answer += "<";
cursor = tagStart + 1;
}
return visible;
return { answer, reasoning };
}
</script>
<template>
<details v-if="showReasoning && reasoning.trim()" class="agent-reasoning-panel" :open="streaming">
<summary>思考过程</summary>
<div class="agent-reasoning-content">{{ reasoning }}</div>
</details>
<div v-if="renderedContent" class="agent-preview-markdown" v-html="renderedContent"></div>
<div v-else-if="streaming" class="agent-preview-generation-state">
<span></span><span></span><span></span>

View File

@@ -222,6 +222,8 @@ export const api = {
export async function streamDebugAgent(
payload: Record<string, unknown>,
onChunk: (chunk: string) => void,
onReasoning: (chunk: string) => void,
onStatus: (status: { message: string; reasoningVisible: boolean }) => void,
onComplete: (result: AgentDebugStreamComplete) => void,
signal?: AbortSignal,
) {
@@ -256,9 +258,14 @@ export async function streamDebugAgent(
const parsed = JSON.parse(data) as AgentDebugStreamComplete & {
type?: string;
content?: string;
reasoningVisible?: boolean;
};
if (parsed.type === "error") throw new Error(parsed.message || "Agent 调试失败");
if (parsed.type === "content" && parsed.content) onChunk(parsed.content);
if (parsed.type === "reasoning" && parsed.content) onReasoning(parsed.content);
if (parsed.type === "status") {
onStatus({ message: parsed.message || "思考中", reasoningVisible: Boolean(parsed.reasoningVisible) });
}
if (parsed.type === "complete") onComplete(parsed);
}
}

View File

@@ -1484,6 +1484,37 @@ textarea {
.agent-preview-generation-state span:nth-child(2) { animation-delay: 0.14s; }
.agent-preview-generation-state span:nth-child(3) { margin-right: 4px; animation-delay: 0.28s; }
.agent-reasoning-panel {
margin: 6px 0 10px;
overflow: hidden;
border: 1px solid #dbe7e3;
border-radius: 10px;
background: #f2f7f5;
color: #496259;
}
.agent-reasoning-panel summary {
min-height: 34px;
display: flex;
align-items: center;
padding: 0 10px;
color: #315d52;
font-size: 12px;
font-weight: 700;
cursor: pointer;
}
.agent-reasoning-content {
max-height: 220px;
overflow-y: auto;
padding: 0 10px 10px;
color: #5d7169;
font-size: 12px;
line-height: 1.65;
white-space: pre-wrap;
word-break: break-word;
}
@keyframes agent-generation-pulse {
0%, 70%, 100% { opacity: 0.25; transform: translateY(0); }
35% { opacity: 1; transform: translateY(-2px); }

View File

@@ -138,6 +138,7 @@ export interface AgentGenerationConfig {
frequencyPenalty: number | null;
maxToken: number;
streamEnabled: number;
reasoningVisible: number;
}
export interface AgentRuntimeConfig extends AgentGenerationConfig {