企业级 LLM 实战:在受限环境中基于 Copilot API 构建 ReAct MCP Agent

企业级 LLM 实战:在受限环境中基于 Copilot API 构建 ReAct MCP Agent

在银行等金融 IT 环境中,LLM 应用落地往往面临着严苛的限制。最典型的一道坎是:我们只能使用公司内部提供的 LLM API(如 Copilot API),而这些 API 往往是不完整的。

本文将复盘一次真实的架构演进:当我们的基础模型不支持标准的 Function Calling (bind_tools) 时,如何通过 ReAct 模式Model Context Protocol (MCP),手动构建一个强大的、支持工具调用的智能 Agent。

1. 交互全景图 (Architecture Overview)

在深入代码细节之前,让我们先通过一张时序图来俯瞰整个系统的请求流转过程。

MCP Server (GitHub)Copilot API (BaseChatModel)GithubReactAgent (Expert)MainAgent (Router)PostgreSQLChatServiceChatRouter (FastAPI)前端用户MCP Server (GitHub)Copilot API (BaseChatModel)GithubReactAgent (Expert)MainAgent (Router)PostgreSQLChatServiceChatRouter (FastAPI)前端用户第一阶段:请求接收与上下文加载第二阶段:主 Agent 路由思考第三阶段:子 Agent 执行与工具调用第四阶段:响应持久化POST /chat ("列出我的 repo")初始化 (Inject GithubAgent)stream_chat_response(request, MainAgent)INSERT User Message ("列出我的 repo")SELECT Chat History (limit=20)History Records去重 (Remove Duplicate User Input)astream("列出我的 repo", history)System Prompt (Tools Def) + History + InputChunk: ```json { "action": "delegate_to_github" } ```拦截 JSON,生成友好提示Yield: "[System: Asking GitHub Agent...]"astream("列出我的 repo")System Prompt (GitHub Tools) + InputChunk: ```json { "action": "get_repo_list" } ```拦截 JSONYield: "[Thinking: Calling get_repo_list...]"call_tool("get_repo_list", args)Tool Result (JSON List)Observation: Tool ResultFinal Answer ("这里是您的仓库列表...")Yield Final AnswerYield Final AnswerStream Response Token by TokenINSERT Assistant Message (Full Response)

2. 困境:当 bind_tools 失效

2.1 背景

我们基于公司提供的 Copilot API 封装了一个 LangChain BaseChatModel。基础的对话功能(ainvoke, astream)一切正常。

2.2 遭遇滑铁卢

当我们试图引入工具调用能力(Agentic Workflow)时,按照标准文档调用 llm.bind_tools(tools),却收到了冷冰冰的错误:
NotImplementedError

原因在于:Copilot API(或其内部封装)并没有完全遵循 OpenAI 的 Function Calling 规范,或者我们的封装层无法透传这些参数。

这意味着我们失去了一键构建 Agent 的能力。我们必须寻找另一条路。

3. 破局:回归 ReAct 模式与核心组件设计

既然模型“不懂”原生工具调用,我们就教它用“人话”来调用工具。这正是 ReAct (Reasoning + Acting) 模式的精髓。

为了实现这一目标,我们设计了以下核心组件:

3.1 McpToolConverter: 协议适配器

职责:将 MCP 协议定义的工具(JSON Schema)转换为 LangChain 的 StructuredTool。这确保了我们的代码能够“读懂”MCP Server 提供的任何工具。

# src/tools/mcp_tool_converter.pyclassMcpToolConverter:@staticmethoddefconvert(tool: McpTool)-> StructuredTool:# 动态创建 Pydantic Model,这是 LangChain 验证参数的基础 fields ={}for name, prop in tool.inputSchema["properties"].items():# ... 解析类型和描述 ... fields[name]=(p_type, Field(description=desc)) args_model = create_model(f"{tool.name}Schema",**fields)return StructuredTool.from_function(..., args_schema=args_model)

3.2 ToolCallableAgent: 抽象基类

职责:负责基础设施。它连接 MCP Server,获取工具列表,并负责生成能够“教”会 LLM 使用这些工具的 System Prompt。

关键实现:手动构建工具 Prompt
既然不能用 bind_tools,我们就把工具定义写进 System Prompt 里。

# src/agents/tool_callable_agent.pyclassToolCallableAgent(BaseAgent):asyncdefinitialize(self):# 1. 连接 MCP Server# 2. 获取工具列表# 3. 生成 Prompt 描述 self.tool_definitions = self._format_tool_definitions(self.tools)def_format_tool_definitions(self, tools: List[McpTool])->str: prompt_lines =["You have access to the following tools:\n"]for tool in tools: schema = json.dumps(tool.inputSchema, indent=2) prompt_lines.append(f"Name: {tool.name}\nDescription: {tool.description}\nArguments: {schema}") prompt_lines.append(""" To use a tool, please output a JSON blob wrapped in markdown code block like this: ...json { "action": "tool_name", "action_input": { ... } } ... """)return"\n".join(prompt_lines)

3.3 GithubReactAgent: 领域专家

职责:专注于 GitHub 相关任务。它继承自 ToolCallableAgent,实现了核心的 ReAct Loop

关键实现:手动解析与执行循环
它不依赖 AgentExecutor,而是自己控制循环逻辑。

# src/agents/github_react_agent.pyclassGithubReactAgent(ToolCallableAgent):def_parse_tool_call(self, text:str)->dict|None:# 正则提取 JSON json_match = re.search(r"```json\s*(\{.*?\})\s*```", text, re.DOTALL)return json.loads(json_match.group(1))if json_match elseNoneasyncdef_agent_loop(self, messages: List)-> AsyncIterator[BaseMessageChunk]:"""ReAct Loop: Think -> Parse -> Act -> Observe -> Think"""while turn < MAX_TURNS:# 1. Thinkasyncfor chunk in self.llm_service.llm.astream(messages):yield chunk # 实时流式输出思考过程# 2. Parse & Actif tool_call := self._parse_tool_call(full_response):# 3. Observe tool_result =await self._execute_tool_ephemeral(tool_call['action'], tool_call['action_input']) messages.append(HumanMessage(content=f"Tool Output: {tool_result}"))

3.4 MainAgent: 智能路由器

职责:作为系统的单一入口,负责意图识别和任务分发。

关键实现:动态路由与幻觉抑制
它不直接执行业务逻辑,而是通过 delegate_to_github 这样的“元工具”将任务派发给 GithubReactAgent。我们在调试中发现它容易产生幻觉,因此对其进行了特别强化。

# src/agents/main_agent.pyclassMainAgent(BaseAgent):def__init__(self, llm_service, github_agent): self.tool_mapping ={"delegate_to_github":{"agent": github_agent,"name":"GitHub Agent"}}def_build_system_prompt(self)->str:# 强指令防止幻觉return"""You are a helpful assistant and a router. CRITICAL INSTRUCTIONS: 1. You MUST ONLY use the tools listed above. 2. Do NOT invent or hallucinate new tools. 3. If the user request involves GitHub ..., MUST use `delegate_to_github`. """asyncdef_astream_impl(self,input, chat_history):# ... (流式输出与 JSON 拦截逻辑) ...# 如果检测到 JSON Tool Call,拦截并替换为友好提示if tool_call:yield AIMessageChunk(content=f"\n[System: I will ask the {agent_name} to help...]\n")asyncfor chunk in agent.astream(query):yield chunk 

4. 进阶挑战:调试与修复

解决了“能用”的问题后,我们又遇到了“好用”的问题。

4.1 场景:分步提问引发的血案

用户先问:“列出我的 repo”,Agent 问:“你是谁?”,用户答:“nvd11”。
在这个过程中,我们遇到了两个严重问题:

  1. 重复提问:Agent 似乎忘记了它问过什么,或者把用户的回答重复处理了。
  2. 幻觉:Agent 在调用工具前,自己编造了一堆假的 repo 列表。

4.2 调试与修复

通过 LangSmith Trace,我们发现问题的根源在于我们手动实现的 Loop 和 Prompt 还不够严谨。

修复一:历史记录去重
我们的 ChatService 采用了“先存后读”的策略,导致最新的 User Input 在 chat_history 中出现了一次,作为 input 参数又出现了一次。模型看到两次 “nvd11”,逻辑就乱了。

  • Fix: 在读取历史记录后,如果最后一条与当前输入相同,手动移除它。

修复二:幻觉抑制 (Thinking Suppression)
模型太“热心”了,在输出 JSON 工具调用指令的同时,顺便把“结果”也编出来了。

  • Fix 1 (Prompt): 在 MainAgent System Prompt 中加入 CRITICAL INSTRUCTIONS,严厉禁止 “invent or hallucinate new tools”。
  • Fix 2 (Code): 在流式输出 (astream) 中引入拦截机制。一旦检测到 ```json 开始,就停止向用户输出后续文本。只在工具执行完毕后,由系统生成一条友好的 [System: Calling GitHub...] 提示。

5. 总结

在受限的企业级环境中,我们不能总是依赖最先进、最便捷的 API(如 OpenAI Function Calling)。但这并不意味着我们束手无策。

通过 ReAct 模式,我们用最原始的 Prompt Engineering 和正则解析,手动重建了 Agent 的思考回路。结合 MCP 协议,我们成功将这一能力扩展到了无限的外部工具。

这不仅是一个技术 workaround,更是一种对 LLM 原理深刻理解后的架构创新。它证明了:只要模型具备基本的指令遵循能力(Instruction Following),我们就能构建出强大的 Agent 系统。

Read more

前端权限控制设计:别再写死权限判断了

前端权限控制设计:别再写死权限判断了

前端权限控制设计:别再写死权限判断了 毒舌时刻 这代码写得跟网红滤镜似的——仅供参考。 各位前端同行,咱们今天聊聊前端权限控制。别告诉我你还在每个页面写死权限判断,那感觉就像在每个房间都装一把不同的锁——管理起来要命。 为什么你需要权限控制设计 最近看到一个项目,权限判断散落在100个文件里,改一个权限规则要改100处,我差点当场去世。我就想问:你是在做权限控制还是在做权限混乱? 反面教材 // 反面教材:分散的权限判断 // Page1.jsx if (user.role !== 'admin') { return <div>无权限</div>; } // Page2.jsx if (!user.permissions.includes('user:view')) { return <div>

前端多版本零404部署实践:为什么会404,以及怎么彻底解决

这是一篇给“小白也能看懂”的实践文:讲清现象、根因、方案选择与我们的落地实现。 1. 现象:为什么发布新版本后会出现 404? 一个真实场景: * 10:00 用户打开了你的网页(加载的是 v1.0.4 的 HTML) * 10:10 你发布了 v1.0.5 * 用户没有刷新页面,继续点击某个功能 * 页面尝试按旧 HTML 里的地址加载某个 chunk:/assets/pages-about-about.DK5VADjQ.js * 服务器上只剩 v1.0.5 的文件,旧的被删了 → 直接 404 关键点: * HTML 决定了要加载哪些 JS/CSS(包含具体

前端八股文面经大全:字节跳动前端一面(2025-10-09)·面经深度解析

前端八股文面经大全:字节跳动前端一面(2025-10-09)·面经深度解析

前言 大家好,我是木斯佳。 在这个春节假期,当大家都在谈论返乡、团圆与休息时,作为一名技术人,我的思考却不由自主地转向了行业的「冬」与「春」。 相信很多人都感受到了,在AI浪潮的席卷之下,前端领域的门槛在变高,纯粹的“增删改查”岗位正在肉眼可见地减少。曾经热闹非凡的面经分享,如今也沉寂了许多。但我们都知道,市场的潮水退去,留下的才是真正在踏实准备、努力沉淀的人。学习的需求,从未消失,只是变得更加务实和深入。 正值春节,也是复盘与规划的好时机。结合ZEEKLOG这次「春节代码贺新年」活动所提倡的“用技术视角记录春节、复盘成长”,我决定在这个假期持续更新专栏,帮助年后参加春招的同学。 这个专栏的初衷很简单:拒绝过时的、流水线式的PDF引流贴,专注于收集和整理当下最新、最真实的前端面试资料。我会在每一份面经和八股文的基础上,尝试从面试官的角度去拆解问题背后的逻辑,而不仅仅是提供一份静态的背诵答案。无论你是校招还是社招,目标是中大厂还是新兴团队,只要是真实发生、有价值的面试经历,我都会在这个专栏里为你沉淀下来。 温馨提示:市面上的面经鱼龙混杂,

Flutter 三方库 webdriver 的鸿蒙化适配指南 - 掌控全自动端向测试、浏览器自动化实战、鸿蒙级精密 QA 专家

Flutter 三方库 webdriver 的鸿蒙化适配指南 - 掌控全自动端向测试、浏览器自动化实战、鸿蒙级精密 QA 专家

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net Flutter 三方库 webdriver 的鸿蒙化适配指南 - 掌控全自动端向测试、浏览器自动化实战、鸿蒙级精密 QA 专家 在鸿蒙跨平台应用执行复杂的 Web 自动化测试(如模拟用户在高并发下的登录流程、处理复杂的 DOM 树抓取或是实现一个具备全自动回测能力的 CI/CD 流水线)时,如果依赖手动测试或简单的 HTTP 拨测,极易在处理“动态元素渲染”、“多窗口会话指控”或“JavaScript 异步执行”时陷入回归测试漏洞。如果你追求的是一种完全对齐 W3C WebDriver 协议规范、支持多种驱动后端且具备极致工程掌控力的方案。今天我们要深度解析的 webdriver——一个专注于浏览器指控的顶级框架,正是帮你打造“鸿蒙超感 QA 中心”的核心重器。 前言