
在实现causalchat功能的时候,遇到了需要反复调用不同的工具的场景,如果只有一个mcp服务,那么也许不需要agent智能体的构建,但是如果一旦加入rag等复杂功能的实现,谁先谁后就不能在代码中进行硬编码了,为此我们将实现一个agent架构 相关文章: 【MCP】小白详解从0开始构建MCP服务端全流程 【RAG+向量数据库】小白从0构建一个rag和向量数据库demo
1. MCP工具的参数封装
前提:对于Langchain中的agent模式,需要一个严格的接口定义,而在 LangChain 中,这个标准接口就是 BaseTool 类。 对于所有工具,agent都需要有以下几个参数
- 它必须是一个 BaseTool 对象:这是最基本的要求。
- 必须有 name 和 description:Agent 通过这些信息来"思考"和决定用哪个工具。
- 必须有 args_schema:这是一个 Pydantic 模型,它精确地告诉 Agent 这个工具需要哪些参数,以及这些参数的类型。这是 Agent 能-够正确生成调用参数的基础。
- 必须有 _arun 方法:这是 Agent 按下工具"启动按钮"的地方。当 Agent 决定使用某个工具时,它就会调用这个工具的 _arun 方法。
有了这个前提,我们再去看我们之前的mcp工具,就会发现几个问题 首先,对于我们之前调用的mcptool,tool包括以下几个参数
python
mcp_tools = [{
"type": "function",
"function": {
"name": tool.name,
"description": tool.description,
"parameters": tool.inputSchema,
}
也就是包括名字,描述,参数输入格式 但是在agent中,这样的json格式已经不能被识别了,也就是我们需要构建一个格式化的接口
首先我们按照以前的教程构建一个mcp服务,这个服务包括以下参数,这里的tool是从mcp服务器返回的原始数据,原始数据如下,假设我们这个mcp只需要输入username和filename(这个是在mcp服务中构建的,详细请参阅上述文章)
python
{
"type": "function",
"function": {
"name": "xx",
"description": "xx",
"inputScheme": {
"username": {
"type": "string"
},
"filename": {
"type": "string"
}
},
"required": ["username"]
}
}
我们将原始数据进行处理为以下这个更加简洁的结构
python
mcp_tools = [{
"type": "function",
"function": {
"name": tool.name,
"description": tool.description,
"parameters": tool.inputSchema,
}
} for tool in tools_response.tools]
mcp_session = session
首先是进行agent中的args_schema构建
python
def create_pydantic(schema: dict, model_name: str) -> Type[BaseModel]:
# 这里创建一个返回字典
fields = {}
# 这里从mcptool的参数进行一步步解析
# 从上述的参数中获取这两个值
properties = schema.get('properties', {})
required_fields = schema.get('required', [])
# 进行字典的解包推导
for prop_name, prop_schema in properties.items():
# 这里对类型做了简化映射,可以根据未来工具的复杂性进行扩展
field_type: Type[Any] = str # 默认为字符串类型
if prop_schema.get('type') == 'integer':
field_type = int
elif prop_schema.get('type') == 'number':
field_type = float
elif prop_schema.get('type') == 'boolean':
field_type = bool
# Pydantic 的 create_model 需要一个元组: (类型, 默认值)
# 对于必需字段,默认值是 ... (Ellipsis)
if prop_name in required_fields:
fields[prop_name] = (field_type, ...)
else:
fields[prop_name] = (field_type, None)
# 使用 Pydantic 的 create_model 动态创建模型类
#
return create_model(model_name, **fields)
create_model 函数在接收到这些参数后,会返回一个全新的类 也就是会创建一个这样的类,用户规定格式
python
class moedelname(BaseModel):
filename:str
username:str
创建完这样一个格式接口之后,我们还需要一个mcp和agent的通信,也就是前面提到的_run 需要定义一个类,继承于BaseTool 这个类首先必须定义四种参数,然后就会利用mcp中的call_tool进行调用mcp,对于返回的文本,直接返回一个纯文本。对于其他格式的mcp,则更具mcp类型返回不同的格式
content: list[TextContent | ImageContent | EmbeddedResource]
python
class Mcptool(Basetool):
name: str
description: str
args_schema: Type[BaseModel] # 强制工具必须有参数结构
session: "ClientSession" # 类型前向引用
# 这里我们使用异步的方法,由于在mcp中采用的是异步处理,防止阻塞主程序,一般来说使用_run即可
# 这里的**kawrg是任意项的参数解包
async def _arun(self,**kwarg:any) -> any:
# 这里同样使用await参数进行异步调用
# 这里就是mcp连接中的call_tool调用
response_obj = await self.session.call_tool(self.name,kwargs)
# 返回纯文本内容
# 这里一班包括了content: list[TextContent | ImageContent | EmbeddedResource]
function_response_text = response_obj.content[0].text
return function_response_text
2. agent主程序的构建
接下来将进行agent主程序的构建
-
获取上下文记录 ,这里可以采用langchain架构的历史记录获取,也可以直接获取数据库中的数据,这里不做讲解
history_messages_raw = get_chat_history(session_id, user_id, limit=20)
-
初始化llm的连接: 获取llm的基础连接细信息,包括密码,模型,温度。注意目前agent不支持流式输出
pythonllm = ChatOpenAI( model=current_model, base_url=BASE_URL, api_key=apikey, temperature=0, streaming=False, )
-
遍历工具箱 首先创建一个tool,这个列表存放着的是一个个类 然后对所有的mcptool进行遍历,遍历完之后,将工具的名称、描述、刚刚创建的参数模型类 (args_schema),以及与服务器的连接 (mcp_session) 等信息,打包成一个 McpTool 类的实例。 最后,将这个 McpTool 实例添加到一个名为 langchain_tools 的列表中。现在,我们就有了一个 LangChain Agent 完全能理解的"工具箱"。
python
langchain_tools: List[BaseTool] = []
for tool_def in mcp_tools:
func_info = tool_def["function"]
# 为动态创建的 Pydantic 模型生成一个唯一的类名
model_name = f"{func_info['name'].replace('_', ' ').title().replace(' ', '')}Input"
# 这里构建一个类,名字的自定义的,参数就是通过mcp_tool获取的
args_schema = create_pydantic(func_info["parameters"], model_name)
# 这里通过类进行初始化
#
langchain_tool = McpTool(
name=func_info["name"],
description=func_info["description"],
args_schema=args_schema,
session=mcp_session,
username=username
)
langchain_tools.append(langchain_tool)
-
创建一个agent prompt 我们将历史记录和userinput传给agent 这里的MessagesPlaceholder(variable_name="agent_scratchpad")指的是agent的内部运作空间
pythonprompt = ChatPromptTemplate.from_messages([ ("system", "你是一个有用的工程师助手,可以使用工具来获取额外信息。" "你的主要任务是分析工具返回的JSON数据,并以详细的自然语言(根据用户给你的语言风格)向用户总结关键发现。" "现在有一下几个要求:1.不要在你的回答中逐字重复整个JSON数据。请根据上下文进行回复。2. 请使用Markdown格式(例如,使用项目符号、加粗、表格等)来组织你的回答 3. 回答需要依照因果推断相关的知识和术语进行回答"), MessagesPlaceholder(variable_name="chat_history"), ("user", "{input}"), MessagesPlaceholder(variable_name="agent_scratchpad"), # Agent 内部工作空间 ])
-
创建 Agent Agent (create_openai_tools_agent):这是一个"大脑"。它由三部分组成:LLM、工具箱(langchain_tools)和提示(Prompt)。它知道如何根据提示和用户的输入来决定调用哪个工具。
pythonagent = create_openai_tools_agent(llm, langchain_tools, prompt)
-
创建执行器 有了agent,我们就开始执行
pythonagent_executor = AgentExecutor( agent=agent, tools=langchain_tools, verbose=True, # verbose=True 会在日志中打印 Agent 的思考过程 return_intermediate_steps=True # 打印中间的工具调用步骤 )
-
格式化历史记录 在langchain中,如果使用其他格式的历史记录,都需要格式化历史记录信息,包括(HumanMessage, AIMessage)
pythonHumanMseeage(content = msg["content"] if msg["role"] = 'user') else AImseeage(conent = mag["content"]) for msg in history
-
调用agent 这部分是agent核心,流程如下
- 思考:执行官将用户的请求、对话历史和工具列表(包含工具的description)一起打包,发送给 LLM。LLM 分析后认为,用户的意图是"某功能",并且 mcp中的工具的描述最匹配这个意图。LLM 还会从用户输入中提取参数,比如 filename。
- 行动 :Agent 决定调用 mcp的对应 工具,并附带提取出的参数。
- 执行:Agent Executor 找到对应的 McpTool 实例,并调用其 _arun 方法。
- 桥接 (_arun 方法):_arun 方法通过 mcp_session 向 mcp_server.py 发起真正的工具调用请求。
- MCP 服务器工作,返回参数或者其他
- 观察 :_arun 方法接收到服务器返回的 JSON,将其作为字符串返回给 Agent Executor。
- 再次思考:Agent Executor 将工具返回的结果(观察)再次发给 LLM,说:"我调用工具后得到了这个 JSON,请你根据它,并结合我们之前的对话,生成一个最终的、对用户友好的回复。"
- 最终输出:通过prompt,LLM 生成一段总结性的文字,例如:"好的,我已经完成了因果分析。结果显示,干预变量对结果变量有显著的正向影响..."。
pythonagent_response = await agent_executor.ainvoke({ "input": text, "chat_history": chat_history }) final_output_summary = agent_response.get("output", "抱歉,我在处理时遇到了问题。")
3. 观察总结agent调用
我们打印出agent的中间过程就会发现包括以下几个部分
-
'chat_history': [ HumanMessage(content='你好', ...), AIMessage(content='你好!有什么我可以 帮助你的吗?', ...), // ... 大量的历史消息 ... ]
这里的就是所有的已经格式化的历史消息 -
'intermediate_steps': [ ( ToolAgentAction(...), '{"success": true, ... }' ) ]
这里就是工具的调用,我们进行拆解 这里的是agent的行动日志,其中包括了一个mcp_tool,然后包括了参数的输出 这整个字典就是传递给 McpTool._arun 方法的 kwargs。pythonToolAgentAction( tool='perform_causal_analysis', tool_input={'filename': '1.csv', 'username': 'test_user'}, log="...", message_log=[...], tool_call_id='' )
接下来就是
'{"success": true, "message": "因果分析成功完成。", "data": {"nodes": [...], "edges": [...]}, "raw_results": {...}, "analyzed_filename": "1.csv"}'
这里就是agent调用完mcp之后的结果,那么我们可以看到这里有McpTool._arun 方法返回的原始字符串。它就是 MCP 服务器 在执行完因果分析后,返回的未经任何处理的 JSON 响应。 -
'output':xx
这是 LLM 在看到了"行动"(Action)和"观察"(Observation)的全过程后,根据 intermediate_steps 中的信息,并遵循你在 prompt 中设定的指示(例如,"不要逐字重复JSON","使用Markdown格式","用因果术语回答"等),为用户精心生成的、易于理解的自然语言回复。