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>