From 97130cfb1e52b8d88d22b0982a5b87a90a0ff67b Mon Sep 17 00:00:00 2001 From: Chen Tao <70054568+eeee0717@users.noreply.github.com> Date: Wed, 7 May 2025 14:34:38 +0800 Subject: [PATCH] feat: optimize knowledge recognize (#5707) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 自由的世界人 <3196812536@qq.com> --- src/renderer/src/i18n/locales/en-us.json | 10 +- src/renderer/src/i18n/locales/ja-jp.json | 8 +- src/renderer/src/i18n/locales/ru-ru.json | 8 +- src/renderer/src/i18n/locales/zh-cn.json | 4 + src/renderer/src/i18n/locales/zh-tw.json | 6 +- .../AssistantKnowledgeBaseSettings.tsx | 39 +++- src/renderer/src/services/ApiService.ts | 214 +++++++++++------- src/renderer/src/types/index.ts | 1 + src/renderer/src/utils/extract.ts | 2 +- 9 files changed, 193 insertions(+), 99 deletions(-) diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index 3da222ee94..65cb423d78 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -9,7 +9,6 @@ "add.prompt": "Prompt", "add.prompt.placeholder": "Enter prompt", "add.title": "Create Agent", - "import.title": "Import from External", "import": { "title": "Import from External", "type": { @@ -78,7 +77,11 @@ "settings.reasoning_effort.low": "Think less", "settings.reasoning_effort.medium": "Think normally", "settings.reasoning_effort.default": "Default", - "settings.more": "Assistant Settings" + "settings.more": "Assistant Settings", + "settings.knowledge_base.recognition.tip": "The assistant will use the large model's intent recognition capability to determine whether to use the knowledge base for answering. This feature will depend on the model's capabilities", + "settings.knowledge_base.recognition": "Use Knowledge Base", + "settings.knowledge_base.recognition.off": "Force Search", + "settings.knowledge_base.recognition.on": "Intent Recognition" }, "auth": { "error": "API key automatically obtained failed, please get it manually", @@ -1540,8 +1543,7 @@ "privacy": { "title": "Privacy Settings", "enable_privacy_mode": "Anonymous reporting of errors and statistics" - }, - "defaultAgent": "Built-in" + } }, "translate": { "any.language": "Any language", diff --git a/src/renderer/src/i18n/locales/ja-jp.json b/src/renderer/src/i18n/locales/ja-jp.json index 5437feb109..fe14a49e4f 100644 --- a/src/renderer/src/i18n/locales/ja-jp.json +++ b/src/renderer/src/i18n/locales/ja-jp.json @@ -9,7 +9,6 @@ "add.prompt": "プロンプト", "add.prompt.placeholder": "プロンプトを入力", "add.title": "エージェントを作成", - "import.title": "外部からインポート", "import": { "title": "外部からインポート", "type": { @@ -78,7 +77,11 @@ "settings.reasoning_effort.low": "少しの思考", "settings.reasoning_effort.medium": "普通の思考", "settings.reasoning_effort.default": "デフォルト", - "settings.more": "アシスタント設定" + "settings.more": "アシスタント設定", + "settings.knowledge_base.recognition.tip": "アシスタントは大規模言語モデルの意図認識能力を使用して、ナレッジベースを参照する必要があるかどうかを判断します。この機能はモデルの能力に依存します", + "settings.knowledge_base.recognition": "ナレッジベースの呼び出し", + "settings.knowledge_base.recognition.off": "強制検索", + "settings.knowledge_base.recognition.on": "意図認識" }, "auth": { "error": "APIキーの自動取得に失敗しました。手動で取得してください", @@ -1540,7 +1543,6 @@ "title": "プライバシー設定", "enable_privacy_mode": "匿名エラーレポートとデータ統計の送信" }, - "defaultAgent": "内蔵", "input.show_translate_confirm": "翻訳確認ダイアログを表示" }, "translate": { diff --git a/src/renderer/src/i18n/locales/ru-ru.json b/src/renderer/src/i18n/locales/ru-ru.json index fe5776109a..6a60701f76 100644 --- a/src/renderer/src/i18n/locales/ru-ru.json +++ b/src/renderer/src/i18n/locales/ru-ru.json @@ -9,7 +9,6 @@ "add.prompt": "Промпт", "add.prompt.placeholder": "Введите промпт", "add.title": "Создать агента", - "import.title": "Импорт из внешнего источника", "delete.popup.content": "Вы уверены, что хотите удалить этого агента?", "edit.message.add.title": "Добавить", "edit.message.assistant.placeholder": "Введите сообщение ассистента", @@ -78,7 +77,11 @@ "settings.reasoning_effort.medium": "Среднее", "settings.reasoning_effort.default": "По умолчанию", "settings.more": "Настройки ассистента", - "settings.reasoning_effort": "Настройки размышлений" + "settings.reasoning_effort": "Настройки размышлений", + "settings.knowledge_base.recognition.tip": "Ассистент будет использовать возможности большой модели для распознавания намерений, чтобы определить, нужно ли обращаться к базе знаний для ответа. Эта функция будет зависеть от возможностей модели", + "settings.knowledge_base.recognition": "Использование базы знаний", + "settings.knowledge_base.recognition.off": "Принудительный поиск", + "settings.knowledge_base.recognition.on": "Распознавание намерений" }, "auth": { "error": "Автоматический получение ключа API не удалось, пожалуйста, получите ключ вручную", @@ -1540,7 +1543,6 @@ "title": "Настройки конфиденциальности", "enable_privacy_mode": "Анонимная отчетность об ошибках и статистике" }, - "defaultAgent": "Встроенный", "input.show_translate_confirm": "Показать диалоговое окно подтверждения перевода" }, "translate": { diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index c4b4a0e9ae..8c405cf4a1 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -68,6 +68,10 @@ "settings.mcp.description": "默认启用的 MCP 服务器", "settings.default_model": "默认模型", "settings.knowledge_base": "知识库设置", + "settings.knowledge_base.recognition.tip": "智能体将调用大模型的意图识别能力,判断是否需要调用知识库进行回答,该功能将依赖模型的能力", + "settings.knowledge_base.recognition": "调用知识库", + "settings.knowledge_base.recognition.off": "强制检索", + "settings.knowledge_base.recognition.on": "意图识别", "settings.model": "模型设置", "settings.preset_messages": "预设消息", "settings.prompt": "提示词设置", diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index 736e6091ed..3e87fd67c3 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -77,7 +77,11 @@ "settings.reasoning_effort.low": "稍微思考", "settings.reasoning_effort.medium": "正常思考", "settings.reasoning_effort.default": "預設", - "settings.more": "助手設定" + "settings.more": "助手設定", + "settings.knowledge_base.recognition.tip": "智慧代理人將調用大語言模型的意圖識別能力,判斷是否需要調用知識庫進行回答,該功能將依賴模型的能力", + "settings.knowledge_base.recognition": "調用知識庫", + "settings.knowledge_base.recognition.off": "強制檢索", + "settings.knowledge_base.recognition.on": "意圖識別" }, "auth": { "error": "自動取得金鑰失敗,請手動取得", diff --git a/src/renderer/src/pages/settings/AssistantSettings/AssistantKnowledgeBaseSettings.tsx b/src/renderer/src/pages/settings/AssistantSettings/AssistantKnowledgeBaseSettings.tsx index 5b64861dbe..169ed3ffd5 100644 --- a/src/renderer/src/pages/settings/AssistantSettings/AssistantKnowledgeBaseSettings.tsx +++ b/src/renderer/src/pages/settings/AssistantSettings/AssistantKnowledgeBaseSettings.tsx @@ -2,7 +2,8 @@ import { CheckOutlined } from '@ant-design/icons' import { Box } from '@renderer/components/Layout' import { useAppSelector } from '@renderer/store' import { Assistant, AssistantSettings } from '@renderer/types' -import { Select, SelectProps } from 'antd' +import { Row, Segmented, Select, SelectProps, Tooltip } from 'antd' +import { CircleHelp } from 'lucide-react' import { useTranslation } from 'react-i18next' import styled from 'styled-components' @@ -46,6 +47,34 @@ const AssistantKnowledgeBaseSettings: React.FC = ({ assistant, updateAssi .includes(input.toLowerCase()) } /> + + + + + + {t('assistants.settings.knowledge_base.recognition.on')} + + + + + ), + value: 'on' + } + ]} + onChange={(value) => + updateAssistant({ + ...assistant, + knowledgeRecognition: value as 'off' | 'on' + }) + } + /> + ) } @@ -57,5 +86,13 @@ const Container = styled.div` overflow: hidden; padding: 5px; ` +const Label = styled.p` + margin-right: 5px; + font-weight: 500; +` +const QuestionIcon = styled(CircleHelp)` + cursor: pointer; + color: var(--color-text-3); +` export default AssistantKnowledgeBaseSettings diff --git a/src/renderer/src/services/ApiService.ts b/src/renderer/src/services/ApiService.ts index 610b515f5a..c801a50b89 100644 --- a/src/renderer/src/services/ApiService.ts +++ b/src/renderer/src/services/ApiService.ts @@ -46,22 +46,32 @@ async function fetchExternalTool( // 可能会有重复? const knowledgeBaseIds = getKnowledgeBaseIds(lastUserMessage) const hasKnowledgeBase = !isEmpty(knowledgeBaseIds) + const knowledgeRecognition = assistant.knowledgeRecognition || 'on' const webSearchProvider = WebSearchService.getWebSearchProvider(assistant.webSearchProviderId) + const shouldWebSearch = !!assistant.webSearchProviderId && webSearchProvider !== null + const shouldKnowledgeSearch = hasKnowledgeBase + + // 在工具链开始时发送进度通知 + const willUseTools = shouldWebSearch || shouldKnowledgeSearch + if (willUseTools) { + onChunkReceived({ type: ChunkType.EXTERNEL_TOOL_IN_PROGRESS }) + } + // --- Keyword/Question Extraction Function --- const extract = async (): Promise => { if (!lastUserMessage) return undefined - // 如果都不需要搜索,则直接返回,不意图识别 - if (!shouldWebSearch && !hasKnowledgeBase) return undefined - // Notify UI that extraction/searching is starting - onChunkReceived({ type: ChunkType.EXTERNEL_TOOL_IN_PROGRESS }) + // 根据配置决定是否需要提取 + const needWebExtract = shouldWebSearch + const needKnowledgeExtract = hasKnowledgeBase && knowledgeRecognition === 'on' - let prompt = '' + if (!needWebExtract && !needKnowledgeExtract) return undefined - if (shouldWebSearch && !hasKnowledgeBase) { + let prompt: string + if (needWebExtract && !needKnowledgeExtract) { prompt = SEARCH_SUMMARY_PROMPT_WEB_ONLY - } else if (!shouldWebSearch && hasKnowledgeBase) { + } else if (!needWebExtract && needKnowledgeExtract) { prompt = SEARCH_SUMMARY_PROMPT_KNOWLEDGE_ONLY } else { prompt = SEARCH_SUMMARY_PROMPT @@ -71,29 +81,20 @@ async function fetchExternalTool( summaryAssistant.model = assistant.model || getDefaultModel() summaryAssistant.prompt = prompt - const getFallbackResult = (): ExtractResults => { - const fallbackContent = getMainTextContent(lastUserMessage) - return { - websearch: shouldWebSearch - ? { - question: [fallbackContent || 'search'] - } - : undefined, - knowledge: hasKnowledgeBase - ? { - question: [fallbackContent || 'search'] - } - : undefined - } as ExtractResults - } - try { const keywords = await fetchSearchSummary({ messages: lastAnswer ? [lastAnswer, lastUserMessage] : [lastUserMessage], assistant: summaryAssistant }) - return keywords ? extractInfoFromXML(keywords) : getFallbackResult() + if (!keywords) return getFallbackResult() + + const extracted = extractInfoFromXML(keywords) + // 根据需求过滤结果 + return { + websearch: needWebExtract ? extracted?.websearch : undefined, + knowledge: needKnowledgeExtract ? extracted?.knowledge : undefined + } } catch (e: any) { console.error('extract error', e) if (isAbortError(e)) throw e @@ -101,16 +102,29 @@ async function fetchExternalTool( } } + const getFallbackResult = (): ExtractResults => { + const fallbackContent = getMainTextContent(lastUserMessage) + return { + websearch: shouldWebSearch ? { question: [fallbackContent || 'search'] } : undefined, + knowledge: shouldKnowledgeSearch + ? { + question: [fallbackContent || 'search'], + rewrite: fallbackContent + } + : undefined + } + } + // --- Web Search Function --- - const searchTheWeb = async (): Promise => { + const searchTheWeb = async (extractResults: ExtractResults | undefined): Promise => { + if (!shouldWebSearch) return + // Add check for extractResults existence early if (!extractResults?.websearch) { console.warn('searchTheWeb called without valid extractResults.websearch') return } - if (!shouldWebSearch) return - // Add check for assistant.model before using it if (!assistant.model) { console.warn('searchTheWeb called without assistant.model') @@ -138,93 +152,121 @@ async function fetchExternalTool( } // --- Knowledge Base Search Function --- - const searchKnowledgeBase = async (): Promise => { - // Add check for extractResults existence early - if (!extractResults?.knowledge) { - console.warn('searchKnowledgeBase called without valid extractResults.knowledge') - return + const searchKnowledgeBase = async ( + extractResults: ExtractResults | undefined + ): Promise => { + if (!hasKnowledgeBase) return + + // 知识库搜索条件 + let searchCriteria: { question: string[]; rewrite: string } + if (knowledgeRecognition === 'off') { + const directContent = getMainTextContent(lastUserMessage) + searchCriteria = { question: [directContent || 'search'], rewrite: directContent } + } else { + // auto mode + if (!extractResults?.knowledge) { + console.warn('searchKnowledgeBase: No valid search criteria in auto mode') + return + } + searchCriteria = extractResults.knowledge } - const shouldSearch = hasKnowledgeBase && extractResults.knowledge.question[0] !== 'not_needed' - - if (!shouldSearch) return + if (searchCriteria.question[0] === 'not_needed') return try { + const tempExtractResults: ExtractResults = { + websearch: undefined, + knowledge: searchCriteria + } // Attempt to get knowledgeBaseIds from the main text block // NOTE: This assumes knowledgeBaseIds are ONLY on the main text block // NOTE: processKnowledgeSearch needs to handle undefined ids gracefully // const mainTextBlock = mainTextBlocks // ?.map((blockId) => store.getState().messageBlocks.entities[blockId]) // .find((block) => block?.type === MessageBlockType.MAIN_TEXT) as MainTextMessageBlock | undefined - return await processKnowledgeSearch(extractResults, knowledgeBaseIds) + return await processKnowledgeSearch(tempExtractResults, knowledgeBaseIds) } catch (error) { console.error('Knowledge base search failed:', error) return } } - const shouldWebSearch = !!assistant.webSearchProviderId - // --- Execute Extraction and Searches --- - const extractResults = await extract() - // Run searches potentially in parallel + let extractResults: ExtractResults | undefined - let webSearchResponseFromSearch: WebSearchResponse | undefined - let knowledgeReferencesFromSearch: KnowledgeReference[] | undefined - const isWebSearchValid = extractResults?.websearch && assistant.model - const isKnowledgeSearchValid = extractResults?.knowledge - const isAllValidSearch = lastUserMessage && (isKnowledgeSearchValid || isWebSearchValid) + try { + // 根据配置决定是否需要提取 + if (shouldWebSearch || hasKnowledgeBase) { + extractResults = await extract() + console.log('Extraction results:', extractResults) + } - if (isAllValidSearch) { - // TODO: 应该在这写search开始 - if (isKnowledgeSearchValid && isWebSearchValid) { + let webSearchResponseFromSearch: WebSearchResponse | undefined + let knowledgeReferencesFromSearch: KnowledgeReference[] | undefined + + // 并行执行搜索 + if (shouldWebSearch || shouldKnowledgeSearch) { ;[webSearchResponseFromSearch, knowledgeReferencesFromSearch] = await Promise.all([ - searchTheWeb(), - searchKnowledgeBase() + searchTheWeb(extractResults), + searchKnowledgeBase(extractResults) ]) - } else if (isKnowledgeSearchValid) { - knowledgeReferencesFromSearch = await searchKnowledgeBase() - } else if (isWebSearchValid) { - webSearchResponseFromSearch = await searchTheWeb() } - // Search判断很准确了,可以在这写search结束 - onChunkReceived({ - type: ChunkType.EXTERNEL_TOOL_COMPLETE, - external_tool: { - webSearch: webSearchResponseFromSearch, - knowledge: knowledgeReferencesFromSearch + + // 存储搜索结果 + if (lastUserMessage) { + if (webSearchResponseFromSearch) { + window.keyv.set(`web-search-${lastUserMessage.id}`, webSearchResponseFromSearch) + } + if (knowledgeReferencesFromSearch) { + window.keyv.set(`knowledge-search-${lastUserMessage.id}`, knowledgeReferencesFromSearch) } - }) - } - - // --- Prepare for AI Completion --- - // Store results temporarily (e.g., using window.keyv like before) - if (lastUserMessage) { - if (webSearchResponseFromSearch) { - window.keyv.set(`web-search-${lastUserMessage.id}`, webSearchResponseFromSearch) } - if (knowledgeReferencesFromSearch) { - window.keyv.set(`knowledge-search-${lastUserMessage.id}`, knowledgeReferencesFromSearch) - } - } - // Get MCP tools (Fix duplicate declaration) - let mcpTools: MCPTool[] = [] // Initialize as empty array - const enabledMCPs = lastUserMessage?.enabledMCPs - if (enabledMCPs && enabledMCPs.length > 0) { - try { - const toolPromises = enabledMCPs.map(async (mcpServer) => { - const tools = await window.api.mcp.listTools(mcpServer) - return tools.filter((tool: any) => !mcpServer.disabledTools?.includes(tool.name)) + // 发送工具执行完成通知 + if (willUseTools) { + onChunkReceived({ + type: ChunkType.EXTERNEL_TOOL_COMPLETE, + external_tool: { + webSearch: webSearchResponseFromSearch, + knowledge: knowledgeReferencesFromSearch + } }) - const results = await Promise.all(toolPromises) - mcpTools = results.flat() // Flatten the array of arrays - } catch (toolError) { - console.error('Error fetching MCP tools:', toolError) } - } - return { mcpTools } + // Get MCP tools (Fix duplicate declaration) + let mcpTools: MCPTool[] = [] // Initialize as empty array + const enabledMCPs = lastUserMessage?.enabledMCPs + if (enabledMCPs && enabledMCPs.length > 0) { + try { + const toolPromises = enabledMCPs.map(async (mcpServer) => { + const tools = await window.api.mcp.listTools(mcpServer) + return tools.filter((tool: any) => !mcpServer.disabledTools?.includes(tool.name)) + }) + const results = await Promise.all(toolPromises) + mcpTools = results.flat() // Flatten the array of arrays + } catch (toolError) { + console.error('Error fetching MCP tools:', toolError) + } + } + + return { mcpTools } + } catch (error) { + console.error('Tool execution failed:', error) + if (isAbortError(error)) throw error + + // 发送错误状态 + if (willUseTools) { + onChunkReceived({ + type: ChunkType.EXTERNEL_TOOL_COMPLETE, + external_tool: { + webSearch: undefined, + knowledge: undefined + } + }) + } + + return { mcpTools: [] } + } } export async function fetchChatCompletion({ diff --git a/src/renderer/src/types/index.ts b/src/renderer/src/types/index.ts index fc122d1da0..fdc02d35f7 100644 --- a/src/renderer/src/types/index.ts +++ b/src/renderer/src/types/index.ts @@ -23,6 +23,7 @@ export type Assistant = { webSearchProviderId?: WebSearchProvider['id'] enableGenerateImage?: boolean mcpServers?: MCPServer[] + knowledgeRecognition?: 'off' | 'on' } export type AssistantMessage = { diff --git a/src/renderer/src/utils/extract.ts b/src/renderer/src/utils/extract.ts index 3b74396f1f..1c764f629a 100644 --- a/src/renderer/src/utils/extract.ts +++ b/src/renderer/src/utils/extract.ts @@ -28,6 +28,6 @@ export const extractInfoFromXML = (text: string): ExtractResults => { } }) const extractResults: ExtractResults = parser.parse(text) - console.log('Extracted results:', extractResults) + // console.log('Extracted results:', extractResults) return extractResults }