在构建企业级智能问答系统时,我们通常面临两个挑战:
- 如何将大语言模型(LLM)的强大理解能力与企业内部知识库结合?
- 如何让模型自动调用工具函数来查询最新、结构化的知识数据?
本篇文章将介绍一个完整的实践方案,结合:
- ✅ MCP(Message Control Protocol):作为函数(工具)服务标准通信协议
- ✅ OpenAI Function Calling:模型自动选择和调用工具的能力
- ✅ 本地知识库
kb.json
:结构化问答数据
构建一个支持函数调用、具备上下文理解能力的智能问答助手(完整代码附在文章末尾,大家自取hh)。
一、项目结构总览
project/
├── data/
│ └── kb.json # 本地知识库问答数据
├── server.py # MCP 服务端:注册知识库工具
├── client.py # MCP 客户端 + OpenAI Function Calling
├── .env # 包含 OpenAI API 密钥等配置
1、知识库内容(kb.json)
[
{
"question": "What is our company's vacation policy?",
"answer": "Full-time employees are entitled to 20 paid vacation days per year..."
},
...
]
该文件是一个结构化的问答列表,包含公司政策、流程指南、制度文档等内容。
2、MCP 工具服务端(server.py)
使用 MCP 框架注册一个函数 get_knowledge_base
,用于返回整个知识库内容。
@mcp.tool()
def get_knowledge_base() -> str:
"""Retrieve the entire knowledge base as a formatted string."""
...
- MCP 会将该函数注册为标准工具,带有
name
、description
和inputSchema
元信息 - 工具会读取
data/kb.json
文件并返回结构化的 Q&A 内容
服务端通过如下方式启动:
mcp = FastMCP("Knowledge Base")
mcp.run(transport="stdio") # 使用 stdio 通信(支持流式调用)
3、客户端集成 OpenAI + MCP 工具(client.py)
关键模块介绍:
1. MCPOpenAIClient 类
- 管理与 MCP 工具服务的连接
- 获取可调用工具并封装为 OpenAI 所需格式
- 自动处理 OpenAI 的工具调用响应
2. 函数调用流程:
response = await openai.chat.completions.create(
model=self.model,
messages=[{"role": "user", "content": query}],
tools=tools,
tool_choice="auto", # 让模型自行决定是否调用工具
)
- 如果模型返回了
tool_calls
,说明它想调用函数 - 客户端解析这些函数调用请求,并通过
self.session.call_tool(...)
执行真正的MCP服务端函数 - 再将函数返回的内容交回模型生成最终自然语言回答
4、运行效果示例
运行 client.py
时,你会看到如下输出:
> python .\client.py
Connected to server with tools:
- get_knowledge_base: Retrieve the entire knowledge base as a formatted string.
Returns:
A formatted string containing all Q&A pairs from the knowledge base.
Query: What is our company's vacation policy?
Response: Here is the summary of the company's vacation policy:
- **Eligibility**: Full-time employees are entitled to 20 paid vacation days per year.
- **Waiting Period**: Vacation days can be taken after completing 6 months of employment.
- **Carryover**: Unused vacation days can be carried over to the next year, up to a maximum of 5 days.
- **Request Process**: Vacation requests should be submitted at least 2 weeks in advance through the HR portal.
Let me know if you need further clarification!
实现了:
- LLM 自动识别用户问题
- 自动调用知识库查询函数
- 返回精确、格式良好的答案
二、背后的通信机制(MCP Stdio)
- 在 MCP 的 stdio 模式中,服务端被客户端作为子进程启动,使用标准输入/输出(stdin/stdout)作为通信接口。
- 客户端通过 stdio_client 建立异步读写流(read_stream / write_stream),与服务端进行双向 JSON-RPC 消息交互,实现工具调用与响应处理。
- 工具函数执行后以 JSON-RPC 响应结构返回
- OpenAI 模型根据响应继续生成自然语言回答
为什么用 MCP 而不是传统 API?
能力 | MCP 工具调用 | REST API |
---|---|---|
自动生成 schema | ✅ | ❌ |
结构化参数支持 | ✅ JSON Schema | 手动定义 |
流式通信 | ✅ 可选(stdio/SSE) | ❌ |
LLM Function Calling 对接 | ✅ 标准结构兼容 | ❌ 不自动对接 |
多工具注册 | ✅ 一次注册,多次使用 | ❌ 手动维护路由和参数 |
三、总结
通过结合 OpenAI Function Calling + MCP 工具调用协议,我们可以让大语言模型具备“自动调用业务函数、查询结构化知识、并自然语言答复”的能力。这是从“通用语言模型”迈向“企业级智能体”的关键一步。
项目实现的功能
能力 | 实现方式 |
---|---|
✅ 支持结构化知识查询 | 本地 kb.json + MCP 工具 |
✅ 支持 OpenAI Function Calling | tool_choice="auto" |
✅ 自动函数调用 + 响应注入 | tool_calls + call_tool + tool message |
✅ 支持多轮上下文与自然语言生成 | openai.chat.completions.create(...) |
✅ 本地通信 & 可扩展部署 | MCP stdio 模式支持子进程管理 |
后续可扩展的方向
读者朋友们可以在此基础上继续扩展,如:
- 增加按关键词模糊查询工具(如
search_knowledge(question)
) - 使用 LangGraph 控制多轮调用流程
- 改为 SSE 或 Streamable HTTP 模式部署为远程知识服务
- 引入嵌入搜索 + 向量检索增强答案准确性
附:项目完整代码
server.py文件内容:
# server.py
import os
import json
from mcp.server.fastmcp import FastMCP
# Create an MCP server
mcp = FastMCP(
name="Knowledge Base",
# host="0.0.0.0",
# port=8050
)
@mcp.tool()
def get_knowledge_base() -> str:
"""Retrieve the entire knowledge base as a formatted string.
Returns:
A formatted string containing all Q&A pairs from the knowledge base.
"""
try:
kb_path = os.path.join(os.path.dirname(__file__), "data", "kb.json")
with open(kb_path, "r") as f:
kb_data = json.load(f)
kb_text = "Here is the retrieved knowledge base:\n\n"
if isinstance(kb_data, list):
for i, item in enumerate(kb_data, 1):
if isinstance(item, dict):
question = item.get("question", "Unkonwn question")
answer = item.get("answer", "Unkonwn answer")
else:
question = f"Item {i}"
answer = str(item)
kb_text += f"Q{i}: {question}\n"
kb_text += f"A{i}: {answer}\n\n"
else:
kb_text += f"Knowledge base content: {json.dumps(kb_data, indent=2)}"
return kb_text
except FileNotFoundError:
return "Error: Knowledge base file not found."
except json.JSONDecodeError:
return "Error: Invalid JSON format in the knowledge base file."
except Exception as e:
return f"Error: {str(e)}"
# Run the server
if __name__ == "__main__":
mcp.run(transport="stdio")
client.py文件内容:
# client.py
import asyncio
import json
from contextlib import AsyncExitStack
from typing import Any, Dict, List, Optional
from dotenv import load_dotenv
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
from openai import AsyncOpenAI
# Load environment variables
load_dotenv("../.env")
class MCPOpenAIClient:
"""Client for interacting with OpenAI models using MCP tools."""
def __init__(self, model: str = "qwen-plus"):
"""Initialize the OpenAI MCP client.
Args:
model: The OpenAI model to use.
"""
# Initialize session and client objects
self.session: Optional[ClientSession] = None
self.exit_stack = AsyncExitStack()
self.openai_client = AsyncOpenAI()
self.model = model
self.stdio: Optional[Any] = None
self.write: Optional[Any] = None
async def connect_to_server(self, server_script_path: str = "server.py"):
"""Connect to an MCP server.
Args:
server_script_path: Path to the server script.
"""
# Server configuration
server_params = StdioServerParameters(
command="python",
args=[server_script_path],
)
# Connect to the server
stdio_transport = await self.exit_stack.enter_async_context(
stdio_client(server_params)
)
self.stdio, self.write = stdio_transport
self.session = await self.exit_stack.enter_async_context(
ClientSession(self.stdio, self.write)
)
# Initialize the connection
await self.session.initialize()
# List available tools
tools_result = await self.session.list_tools()
print("\nConnected to server with tools:")
for tool in tools_result.tools:
print(f" - {tool.name}: {tool.description}")
async def get_mcp_tools(self) -> List[Dict[str, Any]]:
"""Get available tools from the MCP server in OpenAI format.
Returns:
A list of tools in OpenAI format.
"""
tools_result = await self.session.list_tools()
return [
{
"type": "function",
"function": {
"name": tool.name,
"description": tool.description,
"parameters": tool.inputSchema,
},
}
for tool in tools_result.tools
]
async def process_query(self, query: str) -> str:
"""Process a query using OpenAI and available MCP tools.
Args:
query: The user query.
Returns:
The response from OpenAI.
"""
# Get available tools
tools = await self.get_mcp_tools()
# Initial OpenAI API call
response = await self.openai_client.chat.completions.create(
model=self.model,
messages=[{"role": "user", "content": query}],
tools=tools,
tool_choice="auto",
)
# Get assistant's response
assistant_message = response.choices[0].message
# Initialize conversation with user query and assistant response
messages = [
{"role": "user", "content": query},
assistant_message,
]
# Handle tool calls if present
if assistant_message.tool_calls:
# Process each tool call
for tool_call in assistant_message.tool_calls:
# Execute tool call
result = await self.session.call_tool(
tool_call.function.name,
arguments=json.loads(tool_call.function.arguments),
)
# Add tool response to conversation
messages.append(
{
"role": "tool",
"tool_call_id": tool_call.id,
"content": result.content[0].text,
}
)
# Get final response from OpenAI with tool results
final_response = await self.openai_client.chat.completions.create(
model=self.model,
messages=messages,
tools=tools,
tool_choice="none", # Don't allow more tool calls
)
return final_response.choices[0].message.content
# No tool calls, just return the direct response
return assistant_message.content
async def cleanup(self):
"""Clean up resources."""
await self.exit_stack.aclose()
async def main():
"""Main entry point for the client."""
client = MCPOpenAIClient()
await client.connect_to_server("server.py")
# Example: Ask about company vacation policy
query = "What is our company's vacation policy?"
print(f"\nQuery: {query}")
response = await client.process_query(query)
print(f"\nResponse: {response}")
await client.cleanup()
if __name__ == "__main__":
asyncio.run(main())
kb.json文件内容:
[
{
"question": "What is our company's vacation policy?",
"answer": "Full-time employees are entitled to 20 paid vacation days per year. Vacation days can be taken after completing 6 months of employment. Unused vacation days can be carried over to the next year up to a maximum of 5 days. Vacation requests should be submitted at least 2 weeks in advance through the HR portal."
},
{
"question": "How do I request a new software license?",
"answer": "To request a new software license, please submit a ticket through the IT Service Desk portal. Include the software name, version, and business justification. Standard software licenses are typically approved within 2 business days. For specialized software, approval may take up to 5 business days and may require department head approval."
},
{
"question": "What is our remote work policy?",
"answer": "Our company follows a hybrid work model. Employees can work remotely up to 3 days per week. Remote work days must be coordinated with your team and approved by your direct manager. All remote work requires a stable internet connection and a dedicated workspace. Core collaboration hours are 10:00 AM to 3:00 PM EST."
},
{
"question": "How do I submit an expense report?",
"answer": "Expense reports should be submitted through the company's expense management system. Include all receipts, categorize expenses appropriately, and add a brief description for each entry. Reports must be submitted within 30 days of the expense. For expenses over $100, additional documentation may be required. All reports require manager approval."
},
{
"question": "What is our process for reporting a security incident?",
"answer": "If you discover a security incident, immediately contact the Security Team at security@company.com or call the 24/7 security hotline. Do not attempt to investigate or resolve the incident yourself. Document what you observed, including timestamps and affected systems. The Security Team will guide you through the incident response process and may need your assistance for investigation."
}
]
.env文件内容:
OPENAI_API_KEY=your-secret-key
OPENAI_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1