关于langchain调用MCP确保稳定性的小经验

背景

想通过把大模型接入到项目管理工具,做一些统计类的工作:如筛选出提交的缺陷中,信息不全的bug(如没有复现步骤)

代码demo

python 复制代码
"""LangChain (项目)Agent 示例:查看缺陷详情工具。"""

from __future__ import annotations

import asyncio
import json
import os
from pathlib import Path
from typing import Any, Optional, cast

import httpx
import anyio
from dotenv import load_dotenv
from langchain.agents import create_agent
from langchain_core.tools import StructuredTool
from langchain_openai import ChatOpenAI
from pydantic import BaseModel, Field, SecretStr


def _load_env() -> None:
    explicit = (os.getenv("ENV_FILE") or os.getenv("DOTENV_PATH") or "").strip()
    if explicit:
        load_dotenv(dotenv_path=explicit, override=True)
        return

    here = Path(__file__).resolve()
    candidates = [
        here.parent / ".env",
    ]
    for p in candidates:
        if p.exists():
            load_dotenv(dotenv_path=p, override=True)
            return

    load_dotenv(override=False)


_load_env()


def _clean_env_value(value: str | None) -> str:
    if value is None:
        return ""
    cleaned = value.strip()
    while cleaned and cleaned[0] in {"`", "'", '"'} and cleaned[-1] == cleaned[0]:
        cleaned = cleaned[1:-1].strip()
    return cleaned


def _env(key: str, default: str | None = None) -> str:
    v = _clean_env_value(os.getenv(key))
    if v:
        return v
    return "" if default is None else default


def _mcp_url() -> str:
    return _env("MCP_URL")


def _has_openapi_credentials() -> bool:
    return bool(_env("PLUGIN_ID") and _env("PLUGIN_SECRET") and _env("USER_KEY"))


def _print_missing_config() -> None:
    missing: list[str] = []

    if not _env("OPENAI_API_KEY"):
        missing.append("OPENAI_API_KEY")

    if not (_mcp_url() or _has_openapi_credentials()):
        missing.append("MCP_URL(推荐)或 PLUGIN_ID/PLUGIN_SECRET/USER_KEY(三项齐全)")

    if missing:
        print("配置不完整,当前缺少:")
        for k in missing:
            print(f"- {k}")


def _tool_result_to_text(result: Any) -> str:
    content = getattr(result, "content", None)
    if isinstance(content, list):
        texts: list[str] = []
        for block in content:
            if getattr(block, "type", None) == "text":
                text = getattr(block, "text", None)
                if isinstance(text, str):
                    texts.append(text)
        if texts:
            return "\n".join(texts)

        structured = getattr(result, "structuredContent", None)
        if structured is not None:
            return json.dumps(structured, ensure_ascii=False)

        dumped = [b.model_dump(by_alias=True) if hasattr(b, "model_dump") else b for b in content]
        return json.dumps(dumped, ensure_ascii=False)

    return json.dumps(result, ensure_ascii=False, default=str)


def _get_streamable_http_client(mcp_streamable_http_module: Any) -> Any:
    client = getattr(mcp_streamable_http_module, "streamable_http_client", None)
    if client is not None:
        return client
    client = getattr(mcp_streamable_http_module, "streamablehttp_client", None)
    if client is not None:
        return client
    raise AttributeError(
        "mcp.client.streamable_http does not expose streamable_http_client/streamablehttp_client"
    )


def _call_mcp_tool(name: str, arguments: dict[str, Any] | None = None) -> str:
    url = _mcp_url()
    if not url:
        return "未配置 MCP_URL(或 MCP_URL)"

    async def _run() -> str:
        try:
            from mcp.client.session import ClientSession
            import mcp.client.streamable_http as mcp_streamable_http
        except Exception as e:
            raise RuntimeError("缺少依赖:mcp(无法通过 MCP Server 调用项目工具)") from e

        streamable_http_client = _get_streamable_http_client(cast(Any, mcp_streamable_http))
        async with streamable_http_client(url) as (read_stream, write_stream, _):
            async with ClientSession(read_stream, write_stream) as session:
                await session.initialize()
                result = await session.call_tool(name, arguments=arguments)
                return _tool_result_to_text(result)

    return anyio.run(_run, backend="asyncio")


def _list_mcp_tools() -> str:
    url = _mcp_url()
    if not url:
        return "未配置 MCP_URL(或 MCP_URL)"

    async def _run() -> str:
        try:
            from mcp.client.session import ClientSession
            import mcp.client.streamable_http as mcp_streamable_http
        except Exception as e:
            raise RuntimeError("缺少依赖:mcp(无法通过 MCP Server 列出项目工具)") from e

        streamable_http_client = _get_streamable_http_client(cast(Any, mcp_streamable_http))
        async with streamable_http_client(url) as (read_stream, write_stream, _):
            async with ClientSession(read_stream, write_stream) as session:
                await session.initialize()
                tools = await session.list_tools()
                dumped = [t.model_dump(by_alias=True) for t in tools.tools]
                return json.dumps(dumped, ensure_ascii=False, indent=2)

    return anyio.run(_run, backend="asyncio")

class McpCallInput(BaseModel):
    name: str = Field(..., description=" MCP tool 名称,例如 get_item_info")
    arguments: dict[str, Any] = Field(default_factory=dict, description="工具入参(必须严格符合 tool schema)")


async def _run() -> None:
    _print_missing_config()
    if not _env("OPENAI_API_KEY"):
        return
    if not (_mcp_url() or _has_openapi_credentials()):
        return

    llm = cast(Any, ChatOpenAI)(
        api_key=SecretStr(_env("OPENAI_API_KEY")),
        base_url=_env("OPENAI_API_BASE") or None,
        model=_env("LLM_MODEL", "gpt-3.5-turbo"),
        temperature=float(_env("LLM_TEMPERATURE", "0.0")),
        max_completion_tokens=int(_env("LLM_MAX_TOKENS", "800")),
    )

    tools = [
        StructuredTool.from_function(
            func=_list_mcp_tools,
            name="MCP_ListTools",
            description="列出项目 MCP Server 支持的工具(JSON 数组,含 name/description/inputSchema)。",
        ),
        StructuredTool.from_function(
            func=_call_mcp_tool,
            name="MCP_CallTool",
            description="调用项目 MCP Server 的工具。先用 MCP_ListTools 查看工具名与 inputSchema,再调用本工具。",
            args_schema=McpCallInput,
        ),
    ]

    debug = _env("AGENT_DEBUG", "0").lower() in {"1", "true", "yes", "y"}
    agent = create_agent(
        llm,
        tools,
        system_prompt=(
            "你是项目缺陷管理助手。\n"
            "- 如需使用项目 MCP Server 的其他能力,先调用 MCP_ListTools 获取工具与 schema,再用 MCP_CallTool 调用。\n"
            "- 严格根据工具返回结果回答,不要编造信息。\n"
            "- 任何 token/id 必须完整原样传入工具参数,禁止使用 .../... 省略。"
        ),
        debug=debug,
    )

    query = _env("EXAMPLE_QUERY", "查询 缺陷 ID 为 12345 的详情")
    result = await agent.ainvoke({"messages": [{"role": "user", "content": query}]})
    messages = (result or {}).get("messages") or []
    for m in reversed(messages):
        content = getattr(m, "content", None)
        if isinstance(content, str) and content:
            print(content)
            return
    print(result)


def main() -> None:
    asyncio.run(_run())


if __name__ == "__main__":
    main()

经验

  1. 使用上述代码,只给大模型暴露两个工具 _list_mcp_tools和_call_mcp_tool,这样封装调用遵循了MCP原生的协议,相对于langchain最新的简单写法(如下),大模型的工具选择会更加准确
python 复制代码
from langchain_mcp_adapters.client 
import MultiServerMCPClient from langchain.agents 
import create_agent client = MultiServerMCPClient( { "weather": { "transport": "http", "url": "http://localhost:8000/mcp", "headers": { "Authorization": "Bearer YOUR_TOKEN", "X-Custom-Header": "custom-value" }, } } ) 
tools = await client.get_tools() 
agent = create_agent("openai:gpt-4.1", tools) 
response = await agent.ainvoke({"messages": "what is the weather in nyc?"})

2.在模型与工具交互的过程中,需要加入返回处理,工具返回的往往是一些带描述信息的json,需要提取出纯文本给模型,这样会减少大模型的负担从而提高稳定性,如上边代码中的_tool_result_to_text函数的作用

3.StructuredTool是langchain框架提供的json格式的标准化工具,实现的效果为,StructuredTool的工具会用langchain自己的schema校验代码来判断工具的参数是否符合,而不是让llm推理去判断,这样即增加了准确性又减小了llm的负担,稳定性的效果提升明显

4.mcp工具调用的方法使用了大量的异步方法,是要适配mcp.client.streamable_http的异步类型,写法较复杂但是可用

5.很多优化都是从系统提示词或用户提问的提示词中进行的,所以一个清晰的提示词也能大幅增加模型的效果与稳定性

6.有时查询大量数据时,会把llm的上下文撑爆,导致模型变笨,所以针对大量数据的情况要适时总结上下文信息,删除原始数据(或进行外部缓存或持久化存储),保持总结性的关键信息即可(这个在上边的代码没有体现)

相关推荐
billhan20162 小时前
RAG 从零到一:构建你的第一个检索增强生成系统
人工智能
billhan20162 小时前
Function Calling:让大模型连接真实世界
人工智能
程序员飞哥2 小时前
Block科技公司裁员四千人,竟然是因为 AI ?
人工智能·后端·程序员
大模型真好玩2 小时前
大模型训练全流程实战指南工具篇(七)——EasyDataset文档处理流程
人工智能·langchain·deepseek
billhan20163 小时前
Embedding 与向量数据库:语义理解的基础设施
人工智能
OpenBayes贝式计算3 小时前
解决视频模型痛点,TurboDiffusion 高效视频扩散生成系统;Google Streetview 涵盖多个国家的街景图像数据集
人工智能·深度学习·机器学习
OpenBayes贝式计算3 小时前
OCR教程汇总丨DeepSeek/百度飞桨/华中科大等开源创新技术,实现OCR高精度、本地化部署
人工智能·深度学习·机器学习
我要改名叫嘟嘟3 小时前
年后上班三天之后,忽然想作的一次记录
人工智能·程序员
飞哥数智坊4 小时前
SWE-bench 退役:当 AI 评测沦为“刷题游戏”,我们还能信谁?
人工智能