企业级 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

保姆级教程:25个降AI提示词大全,手把手教你去AI味

保姆级教程:25个降AI提示词大全,手把手教你去AI味

保姆级教程:25个降AI提示词大全,手把手教你去AI味 TL;DR:本文整理了25个实测有效的降AI提示词,涵盖角色设定法、语义重构法、口语化改写法等多种技巧,配合嘎嘎降AI等专业工具使用,可以把AI率从92%降到5%以下。每个指令都附带使用场景和效果说明,直接复制就能用。 为什么需要降AI提示词 用DeepSeek、ChatGPT这些AI写论文确实方便,但生成的内容有个致命问题:AI味太重。什么是AI味?简单说就是句式过于工整、用词过于精准、缺乏个人表达痕迹。现在的AIGC检测系统正是抓住这些特征来识别AI生成内容,所以哪怕你让AI帮你写的内容在专业上没问题,检测一看AI率照样飙到90%以上。很多同学的第一反应是手动改,但改来改去AI率还是降不下来,因为你改的只是表面词汇,深层的「机器表达模式」根本没变。这时候就需要用专门的降AI提示词,从源头上让AI输出更「人」的内容。 提示词使用前的准备工作 在开始使用降AI提示词之前,有几件事一定要先做。第一,先检测一下你的原文AI率是多少,心里有个底。如果AI率在30%以下,直接用提示词润色可能就够了;如果在80%以上,建议提示

别等这波 AI 算力浪潮过去才后悔:CANN 应该学什么?

别等这波 AI 算力浪潮过去才后悔:CANN 应该学什么?

别等这波 AI 算力浪潮过去才后悔:CANN 应该学什么? 昇腾 CANN 这几年是真在 “狂飙”,生态越做越大、功能越来越多、文档越写越厚…… 但问题也随之出现: CANN 支持 Python、C++、AscendCL、TBE、MindSpore、PyTorch Frontend、Kernel DSL……这么多"语言",到底学哪个?从哪入门? 别急,今天就给你一次性讲透,看完不再迷茫。 CANN 语言体系到底有多复杂? 整个 CANN 软件栈由多层 API 和 Kernel 构成,所以才会出现一堆「看似不同,实则分工明确」的语言接口 为了简化理解,我们可以把它粗暴分成三层: * 高层:框架调用

为什么顶尖AI公司都在用C++做LLaMA-3推理?深度解析底层性能优势

第一章:为什么顶尖AI公司选择C++进行LLaMA-3推理 在大规模语言模型(LLaMA-3)的部署实践中,性能与资源效率是决定服务响应能力的核心因素。尽管Python在AI研究中占据主导地位,但顶尖科技公司如Meta、NVIDIA和Tesla在生产环境中普遍采用C++实现LLaMA-3的推理引擎,以最大化硬件利用率并降低延迟。 极致的运行时性能 C++允许直接控制内存布局与CPU指令调度,这对于处理LLaMA-3高达数百亿参数的矩阵运算至关重要。通过SIMD指令集和多线程优化,C++能充分释放现代CPU的计算潜力。 零成本抽象与内存管理 与Python的高开销对象模型不同,C++支持编译期多态和RAII机制,能够在不牺牲代码可维护性的前提下消除抽象带来的性能损耗。例如,在加载模型权重时可精确控制内存生命周期: // 使用连续内存块加载权重张量 float* weights = static_cast(aligned_alloc(64, sizeof(float) * tensor_size)); // 避免动态分配开销,提升缓存命中率 for (size_t i = 0

纯文本大模型训练:从BERT到LLaMA系列全覆盖

纯文本大模型训练:从BERT到LLaMA系列的高效实践 在AI技术飞速演进的今天,大模型已不再是实验室里的稀有物种,而是逐步走向企业应用和开发者日常工具链的核心组件。无论是智能客服、自动代码生成,还是知识问答系统,背后都离不开像LLaMA、Qwen、ChatGLM这类大规模语言模型的支持。然而,真正让这些“巨无霸”落地,并非简单加载权重就能完成——训练、微调、对齐、推理、部署,每一个环节都可能成为拦路虎。 尤其是在资源有限的情况下,如何用一张24GB显存的消费级GPU跑通70B参数的模型?如何在不写一行分布式代码的前提下实现跨多卡训练?又该如何快速将一个微调后的模型发布为可用API服务? 这些问题,正是 ms-swift 框架试图解决的核心挑战。作为魔搭社区推出的开源大模型开发框架,它不像传统工具那样只聚焦于某一个环节,而是提供了一套覆盖“预训练→微调→对齐→推理→评测→部署”全生命周期的一站式解决方案。更重要的是,它通过高度抽象的设计,把原本复杂的底层细节封装成简洁接口,让开发者可以专注于任务本身,而非工程实现。 为什么我们需要一个统一的大模型开发框架? 过去几年,Hugg