背景
想通过把大模型接入到项目管理工具,做一些统计类的工作:如筛选出提交的缺陷中,信息不全的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()
经验
- 使用上述代码,只给大模型暴露两个工具 _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的上下文撑爆,导致模型变笨,所以针对大量数据的情况要适时总结上下文信息,删除原始数据(或进行外部缓存或持久化存储),保持总结性的关键信息即可(这个在上边的代码没有体现)