原文链接:https://techcommunity.microsoft.com/blog/azure-ai-foundry-blog/hybrid-ai-using-foundry-local-microsoft-foundry-and-the-agent-framework—part-2/4471983?WT.mc_id=AI-MVP-5003172

本文基于 Microsoft Tech Community 的系列文章 Part 2 做中文整理与改写:这一次不是“本地 agent 需要时调用云端”,而是让 云端托管的 Microsoft Foundry agent 作为主控;当它需要 私密/敏感/与用户强相关 的上下文信息时,再通过 MCP 安全地“回拨”到用户设备旁边运行的 本地 agent。核心目标仍然是:让敏感数据留在本地,只把最小必要的信息以结构化形式提供给云端推理。

说明:文中“症状分诊/医疗建议”等内容仅用于演示与信息说明,不构成医疗建议、诊断或治疗意见。

Part 2:让云端 agent “在需要时向本地要答案”

在 Part 1(本地优先)里,模式是:本地 GPU 上跑的小模型处理原始敏感文本,必要时再调用云端模型获得更强推理能力;主题可以概括为“数据留在本地,智能按需引入”。

Part 2 反过来:

  • 用户主要与 云端托管的 Microsoft Foundry agent 交互
  • 云端 agent 使用强推理模型(示例为 GPT-4.1)进行推理与编排
  • 一旦需要 个人病史/敏感上下文/用户特定信息,云端 agent 通过 MCP 安全调用本地的“Personal Health Coach”
  • 云端永远拿不到完整原始病史;本地只返回 去标识化、PII-free、最小必要 的结构化摘要

原文用一个形象比喻来解释:

  • 云端 agent = “专科医生”
  • 本地 agent = 你信任的“健康教练”,知道你的病史
  • 云端不直接接触你的完整病史
  • 本地只提供支持推理所需的最少信息

这是一种更“企业可用”的混合智能模式:云端负责高价值推理与治理,本地负责隐私与个性化上下文

架构概览

这张图展示了一个典型的混合 AI 工作流:

  • Azure 上的 Microsoft Foundry 托管 agent 接收用户症状,并用 frontier 模型(示例 GPT-4.1)做推理
  • 当需要个人上下文(例如病史)时,云端 agent 通过 dev tunnel 回拨到用户机器上的 本地 MCP server(Health Coach)
  • 本地 MCP server 查询两类本地资源:
    • Foundry Local 提供的本地 GPU 加速小模型(示例 Phi-4-mini)
    • 存储在本机的健康历史 JSON(或文件/内存)
  • 本地只返回云端推理所需的“最小结构化背景信息”,云端再把这份背景与症状结合生成建议

原文也强调了把 agent 托管在 Microsoft Foundry(Azure)上的价值:

  • 在云端集中管理 逻辑、策略、推理引擎
  • 直接集成 Azure 的身份、监控、治理能力
  • 同时把隐私/重资源任务下沉到本地,获得“企业级控制 + 边缘级隐私与效率”的组合

Demo 搭建

1) 创建云端托管 agent

原文在 Microsoft Foundry 的 UI 中创建 agent,并选择 gpt-4.1 作为模型:

并配置了非常严格的 system prompt 指令,明确:

  • 云端模型没有身份与完整病史访问权限
  • 必须通过 MCP 工具 get_patient_background(symptoms) 拉取本地背景
  • 禁止猜测/编造病史,禁止向用户索取敏感信息
  • 工具结果出来后再结合症状给出非紧急分诊建议
  • 必须以 “This is not medical advice.” 结尾

原文 system prompt(保留原文代码块):

You are a medical-specialist reasoning assistant for non-emergency triage.
You do NOT have access to the patient’s identity or private medical history.
A privacy firewall limits what you can see.

A local “Personal Health Coach” LLM exists on the user’s device.
It holds the patient’s full medical history privately.

You may request information from this local model ONLY by calling the MCP tool:
   get_patient_background(symptoms)

This tool returns a privacy-safe, PII-free medical summary, including:
- chronic conditions
- allergies
- medications
- relevant risk factors
- relevant recent labs
- family history relevance
- age group

Rules:
1. When symptoms are provided, ALWAYS call get_patient_background BEFORE reasoning.
2. NEVER guess or invent medical history — always retrieve it from the local tool.
3. NEVER ask the user for sensitive personal details. The local model handles that.
4. After the tool runs, combine:
      (a) the patient_background output
      (b) the user’s reported symptoms
   to deliver high-level triage guidance.
5. Do not diagnose or prescribe medication.
6. Always end with: “This is not medical advice.”

You MUST display the section “Local Health Coach Summary:” containing the JSON returned from the tool before giving your reasoning.

2) 构建本地 MCP Server(本地 LLM + 个人医疗记忆)

原文给出了一个本地 MCP server 的关键实现片段。整体思路是:在本地启动一个极简 HTTP 服务,接受 MCP 风格的 JSON-RPC 请求,并提供一个工具 get_patient_background 给云端调用。

2.1 HTTP JSON-RPC 网关(“MCP Gateway”)

这个组件负责:

  • 监听本地端口
  • 接受 POST 的 JSON-RPC
  • 规范化 payload
  • 路由到 handle_mcp_request()
  • 返回 JSON-RPC 响应
  • 暴露 initializetools/list
class MCPHandler(BaseHTTPRequestHandler):
    def _set_headers(self, status=200):
        self.send_response(status)
        self.send_header("Content-Type", "application/json")
        self.end_headers()

    def do_GET(self):
        self._set_headers()
        self.wfile.write(b"OK")

    def do_POST(self):
        content_len = int(self.headers.get("Content-Length", 0))
        raw = self.rfile.read(content_len)

        print("---- RAW BODY ----")
        print(raw)
        print("-------------------")

        try:
            req = json.loads(raw.decode("utf-8"))
        except:
            self._set_headers(400)
            self.wfile.write(b'{"error":"Invalid JSON"}')
            return

        resp = handle_mcp_request(req)
        self._set_headers()
        self.wfile.write(json.dumps(resp).encode("utf-8"))

2.2 工具契约:get_patient_background

这部分定义了云端 agent 能看到的工具“契约”,行为看起来就像一个云函数:

  • 通过 tools/list 广播工具信息
  • 接收云端传入的参数
  • 把本地推理委托给 GPU LLM
  • 返回结构化 JSON 给云端
def handle_mcp_request(req):
    method = req.get("method")
    req_id = req.get("id")

    if method == "tools/list":
        return {
            "jsonrpc": "2.0",
            "id": req_id,
            "result": {
                "tools": [
                    {
                        "name": "get_patient_background",
                        "description": "Returns anonymized personal medical context using your local LLM.",
                        "inputSchema": {
                            "type": "object",
                            "properties": { "symptoms": {"type": "string"} },
                            "required": ["symptoms"]
                        }
                    }
                ]
            }
        }

    if method == "tools/call":
        tool = req["params"]["name"]
        args = req["params"]["arguments"]

        if tool == "get_patient_background":
            symptoms = args.get("symptoms", "")
            summary = summarize_patient_locally(symptoms)

            return {
                "jsonrpc": "2.0",
                "id": req_id,
                "result": {
                    "content": [
                        {
                            "type": "text",
                            "text": json.dumps(summary)
                        }
                    ]
                }
            }

2.3 本地 GPU LLM 调用(Foundry Local 集成)

个性化发生在本地(不是云端):

  • 通过 Foundry Local 调用本地 GPU 小模型
  • 注入本地私密医疗数据(文件/内存)
  • 生成去标识化、结构化输出
  • 打印日志,便于观察本地推理是否发生
FOUNDRY_LOCAL_BASE_URL = "http://127.0.0.1:52403"
FOUNDRY_LOCAL_CHAT_URL = f"{FOUNDRY_LOCAL_BASE_URL}/v1/chat/completions"
FOUNDRY_LOCAL_MODEL_ID = "Phi-4-mini-instruct-cuda-gpu:5"

def summarize_patient_locally(symptoms: str):
    print("[LOCAL] Calling Foundry Local GPU model...")

    payload = {
        "model": FOUNDRY_LOCAL_MODEL_ID,
        "messages": [
            {"role": "system", "content": PERSONAL_SYSTEM_PROMPT},
            {"role": "user", "content": symptoms}
        ],
        "max_tokens": 300,
        "temperature": 0.1
    }

    resp = requests.post(
        FOUNDRY_LOCAL_CHAT_URL,
        headers={"Content-Type": "application/json"},
        data=json.dumps(payload),
        timeout=60
    )

    llm_content = resp.json()["choices"][0]["message"]["content"]
    print("[LOCAL] Raw content:\n", llm_content)

    cleaned = _strip_code_fences(llm_content)
    return json.loads(cleaned)

3) 打通公网回拨:Azure Dev Tunnels

为了让云端能访问到本地 MCP endpoint,原文使用了 Azure Dev Tunnels。示例用 4 条 PowerShell 命令完成安装与隧道创建:

PS C:\Windows\system32> winget install Microsoft.DevTunnel

PS C:\Windows\system32> devtunnel create mcp-health

PS C:\Windows\system32> devtunnel port create mcp-health -p 8081 --protocol http

PS C:\Windows\system32> devtunnel host mcp-health

随后会得到一个公网 endpoint,例如:

https://xxxxxxxxx.devtunnels.ms:8081

4) 在 Azure AI Foundry UI 中把 MCP 工具接上

原文接着在 Azure AI Foundry 的 UI 里为 agent 添加一个自定义工具(MCP):

并指向上一步创建的 dev tunnel 公网地址:

到这里,基础 wiring 就完成了。

Demo:云端 agent 与本地私密 LLM 对话

原文使用的示例提示词是:

“Hi, I’ve been feeling feverish, fatigued, and a bit short of breath since yesterday. Should I be worried?”

说明:示例中的任何诊断结果或医疗建议仅用于演示与信息说明,不构成医疗建议、诊断或治疗意见。

运行时的实时序列如下图所示:

结论:为什么这个混合模式重要

原文的结论很明确:混合 AI 的价值在于把智能放到“最合适的位置”——高价值推理在云端,敏感/上下文数据在本地。这样既能更好保护隐私,也能降低云端算力成本:大量例行查询、上下文收集、个人历史检索,都可以由轻量本地模型承担,而不是每次都消耗昂贵的 frontier 模型。

更重要的是,这种模式能衍生出许多现实应用:

  • 本地财务数据 + 云端财务分析
  • 设备端的编码知识 + 云端规模化重构
  • 本地企业上下文 + 云端自动化 agent

在工业与边缘场景里,本地 agent 甚至可以部署在传感器、摄像头、终端机、IoT 设备旁边,提供即时且私密的智能;云端则负责更复杂的决策与编排。

混合 AI 让每个设备都能成为“智能协作者”,也让每个云端 agent 成为可以安全利用本地专长的“专家”。

参考链接

完整 demo 仓库:https://github.com/olivierb123/hybrid-ai-mcp-localhealthcoach?WT.mc_id=AI-MVP-5003172