feat(chatbot): 增强智能体工具和知识库集成能力

- 在智能体上下文中添加知识库字段,支持从知识库中选择工具
- 重构工具获取逻辑,支持根据上下文动态加载工具
- 优化前端知识库创建和加载逻辑,移入 store 管理
- 改进工具调用结果渲染,支持知识库工具识别
- 添加 clear-btn 样式改进
This commit is contained in:
Wenjie Zhang 2025-12-30 19:01:45 +08:00
parent 34fbd1a13e
commit 8087ef4b00
10 changed files with 187 additions and 128 deletions

View File

@ -3,6 +3,7 @@ from typing import Annotated
from src.agents.common import BaseContext, gen_tool_info
from src.agents.common.mcp import MCP_SERVERS
from src.knowledge import knowledge_base
from .tools import get_tools
@ -14,7 +15,17 @@ class Context(BaseContext):
metadata={
"name": "工具",
"options": lambda: gen_tool_info(get_tools()), # 这里的选择是所有的工具
"description": "工具列表",
"description": "内置的部分工具,包含 common 工具和本智能体的特有工具(不含 MCP",
},
)
knowledges: list[str] = field(
default_factory=list,
metadata={
"name": "知识库",
"options": lambda: [k["name"] for k in knowledge_base.get_retrievers().values()],
"description": "知识库列表,可以在左侧知识库页面中创建知识库。",
"type": "list", # Explicitly mark as list type for frontend if needed
},
)
@ -23,6 +34,8 @@ class Context(BaseContext):
metadata={
"name": "MCP服务器",
"options": lambda: list(MCP_SERVERS.keys()),
"description": "MCP服务器列表",
"description": (
"MCP服务器列表建议使用支持 SSE 的 MCP 服务器,"
"如果需要使用 uvx 或 npx 运行的服务器,也请在项目外部启动 MCP 服务器,并在项目中配置 MCP 服务器。"),
},
)

View File

@ -2,14 +2,13 @@ from langchain.agents import create_agent
from langchain.agents.middleware import ModelRetryMiddleware
from src.agents.common import BaseAgent, load_chat_model
from src.agents.common.mcp import MCP_SERVERS
from src.agents.common.mcp import get_mcp_tools
from src.agents.common.middlewares import (
DynamicToolMiddleware,
context_aware_prompt,
context_based_model,
inject_attachment_context,
)
from src.agents.common.subagents import calc_agent_tool
from src.agents.common.tools import get_kb_based_tools
from .context import Context
from .tools import get_tools
@ -18,7 +17,7 @@ from .tools import get_tools
class ChatbotAgent(BaseAgent):
name = "智能体助手"
description = "基础的对话机器人,可以回答问题,默认不使用任何工具,可在配置中启用需要的工具。"
capabilities = ["file_upload"] # 支持文件上传功能
capabilities = ["file_upload", "reload_graph"] # 支持文件上传功能和重载图
def __init__(self, **kwargs):
super().__init__(**kwargs)
@ -26,34 +25,48 @@ class ChatbotAgent(BaseAgent):
self.checkpointer = None
self.context_schema = Context
def get_tools(self):
"""返回基本工具"""
base_tools = get_tools()
base_tools.append(calc_agent_tool)
return base_tools
async def get_tools(self, tools: list[str] = None, mcps=None, knowledges=None):
# 1. 基础工具 (从 context.tools 中筛选)
all_basic_tools = get_tools()
selected_tools = []
if tools:
# 创建工具映射表
tools_map = {t.name: t for t in all_basic_tools}
for tool_name in tools:
if tool_name in tools_map:
selected_tools.append(tools_map[tool_name])
# 2. 知识库工具
if knowledges:
kb_tools = get_kb_based_tools(db_names=knowledges)
selected_tools.extend(kb_tools)
# 3. MCP 工具
if mcps:
for server_name in mcps:
mcp_tools = await get_mcp_tools(server_name)
selected_tools.extend(mcp_tools)
return selected_tools
async def get_graph(self, **kwargs):
"""构建图"""
if self.graph:
return self.graph
# 创建动态工具中间件实例,并传入所有可用的 MCP 服务器列表
dynamic_tool_middleware = DynamicToolMiddleware(
base_tools=self.get_tools(), mcp_servers=list(MCP_SERVERS.keys())
)
# 获取上下文配置
context = self.context_schema.from_file(module_name=self.module_name)
# 预加载所有 MCP 工具并注册到 middleware.tools
await dynamic_tool_middleware.initialize_mcp_tools()
# 使用 create_agent 创建智能体,并传入 middleware
# 使用 create_agent 创建智能体
graph = create_agent(
model=load_chat_model("siliconflow/Qwen/Qwen3-235B-A22B-Instruct-2507"), # 默认模型,会被 middleware 覆盖
tools=get_tools(), # 注册基础工具
model=load_chat_model(context.model), # 使用 context 中的模型配置
tools=await self.get_tools(context.tools, context.mcps, context.knowledges),
middleware=[
context_aware_prompt, # 动态系统提示词
inject_attachment_context, # 附件上下文注入LangChain 标准中间件)
inject_attachment_context, # 附件上下文注入
context_based_model, # 动态模型选择
dynamic_tool_middleware, # 动态工具选择(支持 MCP 工具注册)
ModelRetryMiddleware(), # 模型重试中间件
],
checkpointer=await self._get_checkpointer(),
@ -69,4 +82,4 @@ def main():
if __name__ == "__main__":
main()
# asyncio.run(main())
# asyncio.run(main())

View File

@ -6,6 +6,7 @@ import requests
from langchain.tools import tool
from src.agents.common import get_buildin_tools
from src.agents.common.subagents import calc_agent_tool
from src.storage.minio import aupload_file_to_minio
from src.utils import logger
@ -51,4 +52,5 @@ def get_tools() -> list[Any]:
"""获取所有可运行的工具(给大模型使用)"""
tools = get_buildin_tools()
tools.append(text_to_img_demo)
tools.append(calc_agent_tool)
return tools

View File

@ -46,7 +46,11 @@ class BaseContext:
model: Annotated[str, {"__template_metadata__": {"kind": "llm"}}] = field(
default=sys_config.default_model,
metadata={"name": "智能体模型", "options": [], "description": "智能体的驱动模型"},
metadata={
"name": "智能体模型",
"options": [],
"description": "智能体的驱动模型,建议选择 Agent 能力较强的模型,不建议使用小参数模型。"
},
)
@classmethod

View File

@ -11,7 +11,7 @@ from pydantic import BaseModel, Field
from src import config, graph_base, knowledge_base
from src.utils import logger
search = TavilySearch(max_results=10)
search = TavilySearch()
search.metadata = {"name": "Tavily 网页搜索"}
@ -122,11 +122,12 @@ class KnowledgeRetrieverModel(BaseModel):
)
def get_kb_based_tools() -> list:
def get_kb_based_tools(db_names: list[str] | None = None) -> list:
"""获取所有知识库基于的工具"""
# 获取所有知识库
kb_tools = []
retrievers = knowledge_base.get_retrievers()
db_ids = [kb_id for kb_id, kb in retrievers.items() if kb["name"] in db_names] or None
def _create_retriever_wrapper(db_id: str, retriever_info: dict[str, Any]):
"""创建检索器包装函数的工厂函数,避免闭包变量捕获问题"""
@ -185,6 +186,9 @@ def get_kb_based_tools() -> list:
return async_retriever_wrapper
for db_id, retrieve_info in retrievers.items():
if db_ids is not None and db_id not in db_ids:
continue
try:
# 构建工具描述
description = (
@ -227,8 +231,6 @@ def get_buildin_tools() -> list:
tools = []
try:
# 获取所有知识库基于的工具
tools.extend(get_kb_based_tools())
tools.extend(get_static_tools())
from src.agents.common.toolkits.mysql.tools import get_mysql_tools

View File

@ -152,6 +152,7 @@
<a-button
type="link"
size="small"
class="clear-btn"
@click="clearSelection(key)"
v-if="getSelectedCount(key) > 0"
>
@ -819,12 +820,6 @@ const resetConfig = async () => {
color: var(--gray-900);
font-weight: 500;
}
.clear-btn {
padding: 0;
height: auto;
font-size: 12px;
}
}
.select-tools-btn {
@ -1133,6 +1128,19 @@ const resetConfig = async () => {
}
}
.clear-btn {
padding: 0;
height: auto;
font-size: 12px;
font-weight: 600;
color: var(--main-700);
&:hover {
color: var(--main-800);
}
}
//
@media (max-width: 768px) {
.agent-config-sidebar.open {

View File

@ -35,6 +35,7 @@
import { computed, ref } from 'vue';
import BaseToolCall from './BaseToolCall.vue';
import { useAgentStore } from '@/stores/agent';
import { useDatabaseStore } from '@/stores/database';
import WebSearchTool from './tools/WebSearchTool.vue';
import KnowledgeBaseTool from './tools/KnowledgeBaseTool.vue';
@ -53,13 +54,18 @@ const props = defineProps({
});
const agentStore = useAgentStore();
const databaseStore = useDatabaseStore();
const toolName = computed(() => props.toolCall.name || props.toolCall.function?.name || '');
const tool = computed(() => {
const toolsList = agentStore?.availableTools ? Object.values(agentStore.availableTools) : [];
return toolsList.find(t => t.name === toolName.value) || null;
const tool = toolsList.find(t => t.name === toolName.value)
return tool || null;
});
const databases = computed(() => databaseStore.databases || []);
const parseData = (content) => {
if (typeof content === 'string') {
try {
@ -90,17 +96,9 @@ const isTaskResult = computed(() => {
});
const isKnowledgeBaseResult = computed(() => {
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 (hasKnowledgebaseTag && isNotLightrag) {
// const data = parseData(props.toolCall.tool_call_result?.content);
// return Array.isArray(data) && data.length > 0;
return true
}
const databaseInfo = databases.value.find(db => db.name === toolName.value);
if (databaseInfo && databaseInfo.kb_type !== 'lightrag') {
return true
}
return false;
});

View File

@ -1,9 +1,7 @@
<script setup>
import { ref, reactive, onMounted, useTemplateRef, computed, provide } from 'vue'
import { RouterLink, RouterView, useRoute } from 'vue-router'
import {
GithubOutlined,
} from '@ant-design/icons-vue'
import { GithubOutlined } from '@ant-design/icons-vue'
import { Bot, Waypoints, LibraryBig, BarChart3, CircleCheck } from 'lucide-vue-next';
import { onLongPress } from '@vueuse/core'
@ -69,7 +67,7 @@ const getRemoteConfig = () => {
}
const getRemoteDatabase = () => {
databaseStore.getDatabaseInfo(undefined, false) // Explicitly load query params for remote database
databaseStore.loadDatabases()
}
// Fetch GitHub stars count

View File

@ -5,21 +5,25 @@ import { message, Modal } from 'ant-design-vue';
import { databaseApi, documentApi, queryApi } from '@/apis/knowledge_api';
import { useTaskerStore } from '@/stores/tasker';
import { useRouter } from 'vue-router';
import { parseToShanghai } from '@/utils/time';
export const useDatabaseStore = defineStore('database', () => {
const router = useRouter();
const taskerStore = useTaskerStore();
// State
const databases = ref([]);
const database = ref({});
const databaseId = ref(null);
const selectedFile = ref(null);
const queryParams = ref([]);
const meta = reactive({});
const selectedRowKeys = ref([]);
const selectedRowKeys = ref([]);
const state = reactive({
listLoading: false,
creating: false,
databaseLoading: false,
refrashing: false,
searchLoading: false,
@ -38,6 +42,64 @@ export const useDatabaseStore = defineStore('database', () => {
let autoRefreshManualOverride = false; // Indicates user explicitly disabled auto-refresh
// Actions
async function loadDatabases() {
state.listLoading = true;
try {
const data = await databaseApi.getDatabases();
databases.value = data.databases.sort((a, b) => {
const timeA = parseToShanghai(a.created_at);
const timeB = parseToShanghai(b.created_at);
if (!timeA && !timeB) return 0;
if (!timeA) return 1;
if (!timeB) return -1;
return timeB.valueOf() - timeA.valueOf(); // 降序排列,最新的在前面
});
} catch (error) {
console.error('加载数据库列表失败:', error);
if (error.message.includes('权限')) {
message.error('需要管理员权限访问知识库');
}
throw error;
} finally {
state.listLoading = false;
}
}
async function createDatabase(formData) {
// 验证
if (!formData.database_name?.trim()) {
message.error('数据库名称不能为空');
return false;
}
if (!formData.kb_type) {
message.error('请选择知识库类型');
return false;
}
// 向量数据库的重排序模型验证
if (['chroma', 'milvus'].includes(formData.kb_type)) {
if (formData.reranker_config?.enabled && !formData.reranker_config?.model) {
message.error('请选择重排序模型');
return false;
}
}
state.creating = true;
try {
const data = await databaseApi.createDatabase(formData);
message.success('创建成功');
await loadDatabases(); // 刷新列表
return data;
} catch (error) {
console.error('创建数据库失败:', error);
message.error(error.message || '创建失败');
throw error;
} finally {
state.creating = false;
}
}
async function getDatabaseInfo(id, skipQueryParams = false) {
const db_id = id || databaseId.value;
if (!db_id) return;
@ -404,6 +466,7 @@ export const useDatabaseStore = defineStore('database', () => {
}
return {
databases,
database,
databaseId,
selectedFile,
@ -411,6 +474,8 @@ export const useDatabaseStore = defineStore('database', () => {
meta,
selectedRowKeys,
state,
loadDatabases,
createDatabase,
getDatabaseInfo,
updateDatabaseInfo,
deleteDatabase,

View File

@ -1,6 +1,6 @@
<template>
<div class="database-container layout-container">
<HeaderComponent title="文档知识库" :loading="state.loading">
<HeaderComponent title="文档知识库" :loading="dbState.listLoading">
<template #actions>
<a-button type="primary" @click="state.openNewDatabaseModel=true">
新建知识库
@ -8,7 +8,7 @@
</template>
</HeaderComponent>
<a-modal :open="state.openNewDatabaseModel" title="新建知识库" @ok="createDatabase" @cancel="cancelCreateDatabase" class="new-database-modal" width="800px">
<a-modal :open="state.openNewDatabaseModel" title="新建知识库" @ok="handleCreateDatabase" @cancel="cancelCreateDatabase" class="new-database-modal" width="800px">
<!-- 知识库类型选择 -->
<h3>知识库类型<span style="color: var(--color-error-500)">*</span></h3>
@ -162,12 +162,12 @@
</div>
<template #footer>
<a-button key="back" @click="cancelCreateDatabase">取消</a-button>
<a-button key="submit" type="primary" :loading="state.creating" @click="createDatabase">创建</a-button>
<a-button key="submit" type="primary" :loading="dbState.creating" @click="handleCreateDatabase">创建</a-button>
</template>
</a-modal>
<!-- 加载状态 -->
<div v-if="state.loading" class="loading-container">
<div v-if="dbState.listLoading" class="loading-container">
<a-spin size="large" />
<p>正在加载知识库...</p>
</div>
@ -235,11 +235,12 @@
<script setup>
import { ref, onMounted, reactive, watch, computed } from 'vue'
import { useRouter, useRoute } from 'vue-router';
import { storeToRefs } from 'pinia';
import { useConfigStore } from '@/stores/config';
import { message } from 'ant-design-vue'
import { Database, Zap, FileDigit, Waypoints, Building2 } from 'lucide-vue-next';
import { useDatabaseStore } from '@/stores/database';
import { Database, FileDigit, Waypoints, Building2 } from 'lucide-vue-next';
import { LockOutlined, InfoCircleOutlined, QuestionCircleOutlined, PlusOutlined } from '@ant-design/icons-vue';
import { databaseApi, typeApi } from '@/apis/knowledge_api';
import { typeApi } from '@/apis/knowledge_api';
import HeaderComponent from '@/components/HeaderComponent.vue';
import ModelSelectorComponent from '@/components/ModelSelectorComponent.vue';
import EmbeddingModelSelector from '@/components/EmbeddingModelSelector.vue';
@ -248,12 +249,13 @@ import AiTextarea from '@/components/AiTextarea.vue';
const route = useRoute()
const router = useRouter()
const databases = ref([])
const configStore = useConfigStore()
const databaseStore = useDatabaseStore()
// 使 store
const { databases, state: dbState } = storeToRefs(databaseStore)
const state = reactive({
loading: false,
creating: false,
openNewDatabaseModel: false,
})
@ -357,32 +359,6 @@ const loadSupportedKbTypes = async () => {
// configStore.config.reranker_names
const loadDatabases = () => {
state.loading = true
// loadGraph()
databaseApi.getDatabases()
.then(data => {
console.log(data)
//
databases.value = data.databases.sort((a, b) => {
const timeA = parseToShanghai(a.created_at)
const timeB = parseToShanghai(b.created_at)
if (!timeA && !timeB) return 0
if (!timeA) return 1
if (!timeB) return -1
return timeB.valueOf() - timeA.valueOf() //
})
state.loading = false
})
.catch(error => {
console.error('加载数据库列表失败:', error);
if (error.message.includes('权限')) {
message.error('需要管理员权限访问知识库')
}
state.loading = false
})
}
const resetNewDatabase = () => {
Object.assign(newDatabase, createEmptyDatabaseForm())
}
@ -473,19 +449,8 @@ const handleLLMSelect = (spec) => {
newDatabase.llm_info.model_name = modelName
}
const createDatabase = () => {
if (!newDatabase.name?.trim()) {
message.error('数据库名称不能为空')
return
}
if (!newDatabase.kb_type) {
message.error('请选择知识库类型')
return
}
state.creating = true
//
const buildRequestData = () => {
const requestData = {
database_name: newDatabase.name.trim(),
description: newDatabase.description?.trim() || '',
@ -496,18 +461,12 @@ const createDatabase = () => {
}
}
//
if (newDatabase.kb_type === 'chroma' || newDatabase.kb_type === 'milvus') {
//
if (['chroma', 'milvus'].includes(newDatabase.kb_type)) {
if (newDatabase.storage) {
requestData.additional_params.storage = newDatabase.storage
}
if (newDatabase.reranker.enabled) {
if (!newDatabase.reranker.model) {
message.error('请选择重排序模型')
state.creating = false
return
}
requestData.additional_params.reranker_config = {
enabled: true,
model: newDatabase.reranker.model,
@ -519,7 +478,6 @@ const createDatabase = () => {
if (newDatabase.kb_type === 'lightrag') {
requestData.additional_params.language = newDatabase.language || 'English'
// LLM
if (newDatabase.llm_info.provider && newDatabase.llm_info.model_name) {
requestData.llm_info = {
provider: newDatabase.llm_info.provider,
@ -528,21 +486,19 @@ const createDatabase = () => {
}
}
databaseApi.createDatabase(requestData)
.then(data => {
console.log('创建成功:', data)
loadDatabases()
resetNewDatabase()
message.success('创建成功')
})
.catch(error => {
console.error('创建数据库失败:', error)
message.error(error.message || '创建失败')
})
.finally(() => {
state.creating = false
state.openNewDatabaseModel = false
})
return requestData
}
//
const handleCreateDatabase = async () => {
const requestData = buildRequestData()
try {
await databaseStore.createDatabase(requestData)
resetNewDatabase()
state.openNewDatabaseModel = false
} catch (error) {
// store
}
}
const navigateToDatabase = (databaseId) => {
@ -585,15 +541,15 @@ watch(
}
)
watch(() => route.path, (newPath, oldPath) => {
watch(() => route.path, (newPath) => {
if (newPath === '/database') {
loadDatabases();
databaseStore.loadDatabases();
}
});
onMounted(() => {
loadSupportedKbTypes()
loadDatabases()
databaseStore.loadDatabases()
// configStore
})