feat(知识库): 完善知识库检索结果展示逻辑和样式

refactor(工具): 更新知识库工具元数据结构
style(UI): 优化配置侧边栏和消息组件样式
docs: 更新开发计划文档
perf(调试): 增强智能体配置调试信息输出
This commit is contained in:
Wenjie Zhang 2025-08-27 00:13:38 +08:00
parent 2e4b8a2363
commit b8ba85654f
8 changed files with 184 additions and 112 deletions

View File

@ -1,6 +1,7 @@
目前已有的开发计划包括:
💭 **Features Todo**
- [ ] LangGraph 升级到 0.6+ 版本,并适配新特性,如 context 等。
- [ ] 使用其他的聊天记录管理方法,解决两个问题,一个是上下文长度过长,一个是上下文的类型变得更加丰富,比如多模态等等。(现在是基于 LangGraph 的 Memory 实现的v0.2.3 版本实现),暂定使用 [mem0](github.com/mem0ai/mem0) 来实现。但是目前了解下来,还不是我想要的那种方案。可能会基于这个实现一个 ThreadConvManager 这个类。
- [ ] 添加对于上传文件的支持这里的复杂的地方就在于如何和历史记录结合在一起v0.2.3 版本实现,放在记忆管理后面)
@ -13,6 +14,8 @@
- [ ] 支持数据库查询功能
- [ ] 消息内容支持图片显示
- [ ] 添加 SQL 读取工具
- [ ] 添加绘图工具(这里的绘图是指绘制固定格式的图和表等,暂时没想好如何支持自定义绘图)
- [ ] 添加测试脚本,覆盖最常见的功能
- [ ] 添加用户日志与用户反馈模块,可以在 AgentView 中查看信息
- [ ] 优化对文档信息的检索展示(检索结果页、详情页)

View File

@ -84,8 +84,8 @@ def get_kb_based_tools() -> dict[str, Any]:
name=tool_id,
description=description,
args_schema=KnowledgeRetrieverModel,
metadata=retrieve_info | {
"tag": "knowledgebase"
metadata=retrieve_info["metadata"] | {
"tag": ["knowledgebase"]
}
)

View File

@ -439,6 +439,7 @@ class KnowledgeBase(ABC):
"name": meta["name"],
"description": meta["description"],
"retriever": make_retriever(db_id),
"metadata": meta
}
return retrievers

View File

@ -205,7 +205,7 @@
<!-- 固定在底部的操作按钮 -->
<div class="sidebar-footer" v-if="!isEmptyConfig">
<div class="form-actions">
<a-button type="primary" @click="saveConfig" class="save-btn">
<a-button @click="saveConfig" class="save-btn" :class="{'changed': agentStore.hasConfigChanges}">
保存配置
</a-button>
<!-- TODOBUG 目前有 bug 暂时不展示 -->
@ -631,14 +631,18 @@ watch(() => props.isOpen, (newVal) => {
.save-btn {
flex: 1;
background: var(--main-color);
background-color: var(--gray-100);
border: none;
border-radius: 6px;
font-weight: 500;
font-size: 14px;
&.changed {
background-color: var(--main-color);
color: #fff;
}
&:hover {
background: var(--main-color);
opacity: 0.9;
}
}

View File

@ -128,7 +128,7 @@ const editorRef = ref()
const emit = defineEmits(['retry', 'retryStoppedMessage', 'openRefs']);
//
const reasoningActiveKey = ref(['show']);
const reasoningActiveKey = ref(['hide']);
const expandedToolCalls = ref(new Set()); //
// store
@ -235,7 +235,7 @@ const toggleToolCall = (toolCallId) => {
.reasoning-box {
margin-top: 10px;
margin-bottom: 15px;
border-radius: 12px;
border-radius: 8px;
border: 1px solid var(--gray-200);
background-color: var(--gray-25);
overflow: hidden;
@ -258,7 +258,7 @@ const toggleToolCall = (toolCallId) => {
// background-color: var(--gray-100);
font-size: 14px;
font-weight: 500;
color: var(--gray-800);
color: var(--gray-700);
// border-bottom: 1px solid var(--gray-200);
transition: all 0.2s ease;
@ -267,7 +267,7 @@ const toggleToolCall = (toolCallId) => {
}
.ant-collapse-expand-icon {
color: var(--main-color);
color: var(--gray-400);
}
}

View File

@ -94,10 +94,11 @@
</template>
<script setup>
import { ref, onMounted, onActivated, onUnmounted, nextTick, reactive, computed, h } from 'vue';
import { ref, onMounted, onActivated, onUnmounted, nextTick, reactive, computed, h, toRaw } from 'vue';
import { useConfigStore } from '@/stores/config';
import { useUserStore } from '@/stores/user';
import { useDatabaseStore } from '@/stores/database';
import { useAgentStore } from '@/stores/agent';
import { useThrottleFn } from '@vueuse/core';
import { message } from 'ant-design-vue';
import {
@ -121,6 +122,7 @@ import { checkAdminPermission } from '@/stores/user';
const configStore = useConfigStore()
const userStore = useUserStore();
const databaseStore = useDatabaseStore();
const agentStore = useAgentStore();
const config = configStore.config;
@ -144,7 +146,6 @@ const state = reactive({
isFullscreen: false,
});
const logs = ref('');
const error = ref('');
const logContainer = ref(null);
let autoRefreshInterval = null;
@ -231,11 +232,6 @@ const onSearch = () => {
// computed
};
//
const filterLogs = () => {
// computed
};
//
const isLogLevelSelected = (level) => {
return state.selectedLevels.includes(level);
@ -376,24 +372,67 @@ const printAgentConfig = async () => {
if (!checkAdminPermission()) return;
try {
console.log('=== 智能体配置 ===');
console.log('=== 智能体配置信息 ===');
//
const agentsData = await agentApi.getAgents();
console.log('智能体列表:', JSON.stringify(agentsData.agents, null, 2));
// store
console.log('Store 状态:', {
isInitialized: agentStore.isInitialized,
selectedAgentId: agentStore.selectedAgentId,
defaultAgentId: agentStore.defaultAgentId,
currentThreadId: agentStore.currentThreadId,
isStreaming: agentStore.isStreaming,
loadingStates: {
isLoadingAgents: agentStore.isLoadingAgents,
isLoadingConfig: agentStore.isLoadingConfig,
isLoadingTools: agentStore.isLoadingTools,
isLoadingThreads: agentStore.isLoadingThreads,
isLoadingMessages: agentStore.isLoadingMessages
},
error: agentStore.error
});
//
const defaultAgent = await agentApi.getDefaultAgent();
console.log('默认智能体:', JSON.stringify(defaultAgent, null, 2));
//
console.log('智能体列表 (从store):', {
count: agentStore.agentsList.length,
agents: toRaw(agentStore.agentsList)
});
//
for (const agent of agentsData.agents) {
try {
const agentConfig = await agentApi.getAgentConfig(agent.id);
console.log(`智能体 "${agent.name}" 配置:`, JSON.stringify(agentConfig, null, 2));
} catch (err) {
console.log(`智能体 "${agent.name}" 配置获取失败:`, err.message);
}
//
if (agentStore.selectedAgent) {
console.log('当前选中智能体:', {
agent: toRaw(agentStore.selectedAgent),
isDefault: agentStore.isDefaultAgent,
configSchema: toRaw(agentStore.configSchema),
configurableItems: Object.keys(agentStore.configurableItems).length
});
//
console.log('当前智能体配置:', {
current: toRaw(agentStore.agentConfig),
original: toRaw(agentStore.originalAgentConfig),
hasChanges: agentStore.hasConfigChanges
});
}
//
console.log('可用工具:', toRaw(agentStore.availableTools));
// 线
console.log('线程信息:', {
currentAgentThreads: agentStore.currentAgentThreads.length,
currentThread: agentStore.currentThread ? toRaw(agentStore.currentThread) : null,
currentThreadMessages: agentStore.currentThreadMessages.length
});
//
if (agentStore.isStreaming || Object.keys(agentStore.onGoingConv.msgChunks).length > 0) {
console.log('当前对话状态:', {
isStreaming: agentStore.isStreaming,
ongoingChunks: Object.keys(agentStore.onGoingConv.msgChunks).length,
ongoingMessages: agentStore.onGoingConvMessages.length,
conversations: agentStore.conversations.length,
detail: toRaw(agentStore.conversations)
});
}
} catch (error) {
@ -462,7 +501,7 @@ const printAgentConfig = async () => {
}
.auto-refresh-button {
color:white;
color: white;
}
}

View File

@ -38,16 +38,16 @@
v-for="(chunk, index) in fileGroup.chunks"
:key="chunk.id"
class="chunk-item"
:class="{ 'high-relevance': chunk.rerank_score > 0.5 }"
:class="{ 'high-relevance': chunk.score > 0.5 }"
@click="showChunkDetail(chunk, index + 1)"
>
<div class="chunk-summary">
<span class="chunk-index">#{{ index + 1 }}</span>
<div class="chunk-scores">
<span class="score-item">相似度 {{ (chunk.distance * 100).toFixed(0) }}%</span>
<span class="score-item">相似度 {{ (chunk.score * 100).toFixed(0) }}%</span>
<span v-if="chunk.rerank_score" class="score-item">重排序 {{ (chunk.rerank_score * 100).toFixed(0) }}%</span>
</div>
<span class="chunk-preview">{{ getPreviewText(chunk.entity.text) }}</span>
<span class="chunk-preview">{{ getPreviewText(chunk.content) }}</span>
<EyeOutlined class="view-icon" />
</div>
</div>
@ -62,7 +62,7 @@
<!-- 弹窗展示chunk详细信息 -->
<a-modal
v-model:open="modalVisible"
:title="`文档片段 #${selectedChunk?.index} - ${selectedChunk?.data?.file?.filename}`"
:title="`文档片段 #${selectedChunk?.index} - ${selectedChunk?.data?.metadata?.source}`"
width="800px"
:footer="null"
class="chunk-detail-modal"
@ -72,10 +72,10 @@
<div class="detail-scores">
<div class="score-card">
<div class="score-label">相似度分数</div>
<div class="score-value-large">{{ (selectedChunk.data.distance * 100).toFixed(1) }}%</div>
<div class="score-value-large">{{ (selectedChunk.data.score * 100).toFixed(1) }}%</div>
<a-progress
:percent="getPercent(selectedChunk.data.distance)"
:stroke-color="getScoreColor(selectedChunk.data.distance)"
:percent="getPercent(selectedChunk.data.score)"
:stroke-color="getScoreColor(selectedChunk.data.score)"
:show-info="false"
stroke-width="6"
/>
@ -92,13 +92,13 @@
</div>
</div>
<div class="detail-meta">
<span class="meta-item"><DatabaseOutlined /> ID: {{ selectedChunk.data.id }}</span>
<span class="meta-item"><DatabaseOutlined /> ID: {{ selectedChunk.data.metadata.chunk_id || selectedChunk.data.metadata.file_id }}</span>
</div>
</div>
<div class="detail-content">
<h5>文档内容</h5>
<div class="content-text">{{ selectedChunk.data.entity.text }}</div>
<div class="content-text">{{ selectedChunk.data.content }}</div>
</div>
</div>
</a-modal>
@ -128,7 +128,7 @@ const fileGroups = computed(() => {
const groups = new Map()
props.data.forEach(item => {
const filename = item.file.filename
const filename = item.metadata.source
if (!groups.has(filename)) {
groups.set(filename, {
filename,
@ -187,66 +187,71 @@ const getScoreColor = (score) => {
border: 1px solid var(--gray-200);
.kb-header {
padding: 10px 14px;
padding: 12px 16px;
border-bottom: 1px solid var(--gray-200);
background: var(--gray-50);
background: var(--gray-25);
h4 {
margin: 0 0 2px 0;
color: var(--main-color);
margin: 0 0 4px 0;
color: var(--gray-800);
font-size: 14px;
font-weight: 500;
display: flex;
align-items: center;
gap: 6px;
.anticon {
color: var(--main-color);
font-size: 13px;
}
}
.result-summary {
font-size: 11px;
color: var(--gray-600);
font-size: 12px;
color: var(--gray-500);
}
}
.kb-results {
padding: 6px;
padding: 8px;
display: flex;
flex-direction: column;
gap: 4px;
gap: 6px;
}
.file-group {
border: 1px solid var(--gray-200);
border-radius: 6px;
border: 1px solid var(--gray-150);
border-radius: 8px;
background: var(--gray-0);
overflow: hidden;
.file-header {
padding: 10px 12px;
padding: 12px 14px;
display: flex;
align-items: center;
justify-content: space-between;
cursor: pointer;
transition: all 0.15s ease;
background: var(--gray-25);
transition: all 0.2s ease;
background: var(--gray-10);
&:hover {
background: var(--gray-50);
background: var(--gray-25);
}
&.expanded {
background: var(--main-10);
border-bottom: 1px solid var(--gray-200);
background: var(--gray-50);
border-bottom: 1px solid var(--gray-150);
}
.file-info {
display: flex;
align-items: center;
gap: 8px;
gap: 10px;
flex: 1;
min-width: 0;
.anticon {
color: var(--main-color);
color: var(--gray-500);
font-size: 13px;
}
@ -263,17 +268,18 @@ const getScoreColor = (score) => {
.chunk-count {
font-size: 11px;
color: var(--gray-500);
background: var(--gray-100);
padding: 1px 5px;
border-radius: 10px;
color: var(--gray-400);
background: var(--gray-50);
padding: 2px 6px;
border-radius: 12px;
border: 1px solid var(--gray-150);
white-space: nowrap;
}
}
.expand-icon {
color: var(--gray-500);
transition: transform 0.15s ease;
color: var(--gray-400);
transition: transform 0.2s ease;
font-size: 12px;
.rotated {
@ -283,25 +289,25 @@ const getScoreColor = (score) => {
}
.chunks-container {
background: var(--gray-10);
background: var(--gray-0);
}
.chunk-item {
padding: 8px 12px;
padding: 10px 14px;
border-bottom: 1px solid var(--gray-100);
cursor: pointer;
transition: all 0.15s ease;
transition: all 0.2s ease;
&:last-child {
border-bottom: none;
}
&.high-relevance {
background: var(--main-10);
background: var(--main-5);
}
&:hover {
background: var(--main-30);
background: var(--gray-25);
.view-icon {
opacity: 1;
@ -311,34 +317,38 @@ const getScoreColor = (score) => {
.chunk-summary {
display: flex;
align-items: center;
gap: 8px;
gap: 10px;
.chunk-index {
color: var(--main-color);
font-size: 10px;
color: var(--gray-500);
font-size: 11px;
font-weight: 500;
min-width: 18px;
min-width: 20px;
text-align: center;
background: var(--gray-25);
padding: 1px 4px;
border-radius: 4px;
}
.chunk-scores {
display: flex;
gap: 4px;
gap: 6px;
.score-item {
font-size: 11px;
color: var(--gray-700);
// background: var(--main-10);
padding: 1px 4px;
color: var(--gray-600);
background: var(--gray-25);
padding: 1px 5px;
border-radius: 4px;
border: 1px solid var(--gray-100);
white-space: nowrap;
}
}
.chunk-preview {
flex: 1;
font-size: 11px;
color: var(--gray-700);
font-size: 12px;
color: var(--gray-600);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
@ -346,10 +356,10 @@ const getScoreColor = (score) => {
}
.view-icon {
color: var(--main-color);
color: var(--gray-400);
font-size: 12px;
opacity: 0.6;
transition: opacity 0.15s ease;
opacity: 0.5;
transition: opacity 0.2s ease;
}
}
}
@ -386,21 +396,23 @@ const getScoreColor = (score) => {
.score-card {
flex: 1;
padding: 8px 12px;
background: var(--gray-50);
border-radius: 6px;
border: 1px solid var(--gray-200);
padding: 12px 14px;
background: var(--gray-25);
border-radius: 8px;
border: 1px solid var(--gray-150);
.score-label {
font-size: 14px;
color: var(--gray-600);
margin-bottom: 4px;
font-size: 12px;
color: var(--gray-500);
margin-bottom: 6px;
font-weight: 500;
}
.score-value-large {
font-size: 18px;
font-weight: 600;
color: var(--main-color);
color: var(--gray-800);
margin-bottom: 8px;
}
}
}
@ -411,13 +423,13 @@ const getScoreColor = (score) => {
.meta-item {
font-size: 11px;
color: var(--gray-500);
color: var(--gray-400);
display: flex;
align-items: center;
gap: 4px;
.anticon {
color: var(--main-color);
color: var(--gray-500);
}
}
}
@ -437,10 +449,10 @@ const getScoreColor = (score) => {
color: var(--gray-700);
white-space: pre-wrap;
word-break: break-word;
background: var(--gray-50);
background: var(--gray-25);
padding: 16px;
border-radius: 6px;
border: 1px solid var(--gray-200);
border-radius: 8px;
border: 1px solid var(--gray-150);
max-height: 400px;
overflow-y: auto;
}

View File

@ -44,6 +44,9 @@ import WebSearchResult from './WebSearchResult.vue'
import KnowledgeBaseResult from './KnowledgeBaseResult.vue'
import KnowledgeGraphResult from './KnowledgeGraphResult.vue'
import CalculatorResult from './CalculatorResult.vue'
import { useAgentStore } from '@/stores/agent';
const agentStore = useAgentStore()
const props = defineProps({
toolName: {
@ -56,6 +59,11 @@ const props = defineProps({
}
})
const tool = computed(() => {
return agentStore?.availableTools?.[props.toolName] || null
})
//
const parsedData = computed(() => {
if (typeof props.resultContent === 'string') {
@ -87,23 +95,28 @@ const isWebSearchResult = computed(() => {
//
const isKnowledgeBaseResult = computed(() => {
const toolNameLower = props.toolName.toLowerCase()
const isKnowledgeBaseTool = toolNameLower.includes('retrieve') ||
toolNameLower.includes('knowledge')
// metadata
const currentTool = tool.value
if (currentTool && currentTool.metadata) {
const metadata = currentTool.metadata
const hasKnowledgebaseTag = metadata.tag && metadata.tag.includes('knowledgebase')
const isNotLightrag = metadata.kb_type !== 'lightrag'
if (!isKnowledgeBaseTool) return false
if (hasKnowledgebaseTag && isNotLightrag) {
const data = parsedData.value
return Array.isArray(data) &&
data.length > 0 &&
data.every(item =>
item &&
typeof item === 'object' &&
'content' in item &&
'score' in item &&
'metadata' in item
)
}
}
const data = parsedData.value
return Array.isArray(data) &&
data.length > 0 &&
data.every(item =>
item &&
typeof item === 'object' &&
'id' in item &&
'distance' in item &&
'entity' in item &&
'file' in item
)
return false
})
//