课程首页 › Module 7 · 框架与场景:MAF / CUA / 本地 Agent › L18

L18 本地 Agent:把 Agent 跑在"自家电脑"上

课程目标:理解本地 Agent 的核心价值与权衡,掌握 Foundry Local + Qwen 的本地 Agent 实战,能用 Chroma 做本地 RAG。


一、为什么需要本地 Agent?#

云端 Agent 多好——但有些场景必须跑在本地:

场景为什么
医疗病历分析隐私——病人数据不能上云
律师审阅合同机密——客户文档不能泄露
工厂车间巡检离线——没网也得能工作
学生代码作业成本——每个学生都调用 GPT 烧钱
个人代码助手隐私——公司代码不能传给 OpenAI

Creating Local AI Agents 把这种 Agent 叫 Local AI Agents:

把 SLM(小语言模型)跑在你自己的机器上,本地推理、本地 RAG、本地工具、本地 MCP——完全不依赖云。


二、用一个比喻:本地 Agent = "自家厨房"#

把 Agent 比作餐饮:

餐饮Agent
餐厅(点外卖)云端 Agent(API 调用)
自家厨房做饭本地 Agent(跑在自己机器)
食材新鲜可控数据不出本机
想做什么做什么不受 API 限制
不需要外卖费零 token 成本
但要自己买菜买锅需要本地硬件(GPU/RAM)

自家厨房 = 自由 + 隐私,但前期投入大、做饭慢。


三、SLM:小模型也能做 Agent#

Creating Local AI Agents 强调一个关键反转:

本地 Agent 用的是 SLM(Small Language Model),不是 GPT-5 这种大模型。

3.1 SLM 的强项#

3.2 SLM 的弱项#

3.3 关键策略:让 SLM 编排,让工具干活#

用户 ──> 本地 SLM Agent ──> 工具 1(读文件)
                    ├──> 工具 2(搜文档 RAG)
                    └──> 工具 3(分析代码)
                              │
                              ▼
                       答案(全程离线)

核心思想:SLM 做"调度员"——它不需要有所有知识,它只需要知道哪个工具能回答。


四、Foundry Local:微软的本地模型运行时#

Creating Local AI Agents 介绍了 Microsoft Foundry Local——一个轻量级运行时:

# Windows 安装
winget install Microsoft.FoundryLocal

# macOS 安装
brew install microsoft/foundrylocal/foundrylocal

# 下载并运行一个 Qwen 模型
foundry model run qwen2.5-7b-instruct

# 查看本地服务状态
foundry service status

本地端点:通常是 http://localhost:PORT/v1——完全兼容 OpenAI API。

这意味着:你的云端 Agent 代码改一行 base_url 就能跑本地。


五、Qwen Function Calling:本地也能调工具#

Creating Local AI Agents 强调——Qwen 模型是专门为 Function Calling 训练的,本地工具调用也能稳定工作。

5.1 连接本地 Qwen#

from foundry_local import FoundryLocalManager
from openai import OpenAI

# Foundry Local 自动发现/下载模型并提供本地端点
manager = FoundryLocalManager("qwen2.5-7b-instruct")

client = OpenAI(
    base_url=manager.endpoint,
    api_key=manager.api_key  # 本地占位符,无真实密钥
)

5.2 注册工具#

def read_file(path: str) -> str:
    """Read a file, but only inside the sandboxed project directory."""
    full = (PROJECT_ROOT / path).resolve()
    if PROJECT_ROOT not in full.parents and full != PROJECT_ROOT:
        return "Access denied: path is outside the project directory."
    return full.read_text(encoding="utf-8")

安全设计:即使本地也要沙箱化——工具不能读取任意路径。

5.3 让 Agent 调用工具#

response = client.chat.completions.create(
    model="qwen2.5-7b-instruct",
    messages=[{"role": "user", "content": "What does auth.py do?"}],
    tools=[{
        "type": "function",
        "function": {
            "name": "read_file",
            "description": "Read a file in the project",
            "parameters": {
                "type": "object",
                "properties": {"path": "the file to read"}
            }
        }
    }]
)

# Qwen 会输出结构化的 tool_call
tool_call = response.choices[0].message.tool_calls[0]
print(tool_call.function.name)  # "read_file"
print(tool_call.function.arguments)  # {"path": "auth.py"}

六、本地 RAG:用 Chroma 做嵌入式向量数据库#

Creating Local AI Agents 用 Chroma 做本地 RAG——它是一个进程内嵌入的向量数据库:

import chromadb

# 初始化(数据存到本地磁盘)
chroma_client = chromadb.PersistentClient(path="./chroma_db")
collection = chroma_client.get_or_create_collection("docs")

# 把文档灌入
collection.add(
    documents=["Authentication module uses JWT...", "Database module uses SQLAlchemy..."],
    ids=["doc1", "doc2"]
)

# 查询
results = collection.query(
    query_texts=["how does auth work?"],
    n_results=2
)

完整 RAG 流程:

本地文档 → 本地 embedding → Chroma
                                 ↓
用户问题 → 本地 embedding → 相似度搜索 → top-k chunks
                                 ↓
                         本地 SLM 生成答案

七、本地 MCP:协议和云无关#

MCP 是传输协议,不是云服务。MCP Server 可以跑在本地:

# 启动本地 MCP Server(stdio 模式)
python my_local_mcp_server.py

Agent 通过 stdio 连接它——完全本地。


八、云-端混合架构#

Creating Local AI Agents 给出了云-端混合的设计模式:

请求 → {敏感或离线?} ── yes ──> 本地 SLM
       {                    ── no ──> {需要深度推理?}
        {                                  │
        {                                  ├── no ──> 本地 SLM(便宜快)
        not needed                          └── yes ──> 云端大模型
场景跑在哪
敏感数据 / 离线本地 SLM
简单 / 有界任务本地 SLM(便宜快)
复杂多跳推理(非敏感)云端大模型
云服务宕机时本地 SLM(优雅降级)

九、动手:本地代码工程助手#

from pathlib import Path
import chromadb
from foundry_local import FoundryLocalManager
from openai import OpenAI

# 1. 设置项目根 + 工具
PROJECT_ROOT = Path("./my_project")

def read_file(path: str) -> str:
    full = (PROJECT_ROOT / path).resolve()
    if PROJECT_ROOT not in full.parents:
        return "Access denied"
    return full.read_text(encoding="utf-8")

def list_files(directory: str = ".") -> list[str]:
    full = (PROJECT_ROOT / directory).resolve()
    if PROJECT_ROOT not in full.parents:
        return ["Access denied"]
    return [str(p.relative_to(PROJECT_ROOT)) for p in full.iterdir()]

# 2. 本地 RAG(Chroma)
chroma_client = chromadb.PersistentClient(path="./chroma_db")
collection = chroma_client.get_or_create_collection("project_docs")

def search_docs(query: str) -> list[str]:
    results = collection.query(query_texts=[query], n_results=3)
    return results["documents"][0]

# 3. 连接本地 Qwen
manager = FoundryLocalManager("qwen2.5-7b-instruct")
client = OpenAI(base_url=manager.endpoint, api_key=manager.api_key)

# 4. Agent 循环
def chat(user_input: str) -> str:
    response = client.chat.completions.create(
        model="qwen2.5-7b-instruct",
        messages=[{"role": "user", "content": user_input}],
        tools=[
            {"type": "function", "function": {"name": "read_file", ...}},
            {"type": "function", "function": {"name": "list_files", ...}},
            {"type": "function", "function": {"name": "search_docs", ...}}
        ]
    )

    # 处理工具调用
    tool_calls = response.choices[0].message.tool_calls
    if tool_calls:
        results = []
        for tc in tool_calls:
            if tc.function.name == "read_file":
                results.append(read_file(**json.loads(tc.function.arguments)))
            elif tc.function.name == "list_files":
                results.append(list_files(**json.loads(tc.function.arguments)))
            elif tc.function.name == "search_docs":
                results.append(search_docs(**json.loads(tc.function.arguments)))

        # 把工具结果喂回去
        response = client.chat.completions.create(
            model="qwen2.5-7b-instruct",
            messages=[
                {"role": "user", "content": user_input},
                response.choices[0].message,
                *[{"role": "tool", "tool_call_id": tc.id, "content": str(r)}
                  for tc, r in zip(tool_calls, results)]
            ]
        )

    return response.choices[0].message.content

十、本地 Agent 的局限#

Creating Local AI Agents 也诚实地列出了局限:

局限缓解
推理质量选合适的 SLM(Qwen 7B 在工具调用上已不错)
响应速度用 GPU/NPU 加速
知识范围用 RAG 补充私域知识
多语言选支持多语言的 SLM
硬件成本8GB RAM 起步,16GB 更舒服

十一、关键要点回顾#

概念内容
SLM小语言模型(本地运行的核心)
Foundry Local微软的本地推理运行时
Qwen Function Calling本地也能调用工具
Chroma嵌入式向量数据库(本地 RAG)
本地 MCP协议和云无关
云-端混合敏感/简单本地,复杂/非敏感云端

核心策略:让 SLM 编排,让工具干活——SLM 不需要懂所有知识。


十二、参考资源#


十三、下一课预告#

L22 安全的 Agent:审计、加密签名、零信任——怎么让 Agent 的每个动作都可验证、不可抵赖。

为什么重要:当 Agent 帮你执行退款、转账、改数据时,谁来保证它没作弊、没篡改?这就是加密签名的用武之地。

自测 · 检验是否真的掌握了

1. 本地 Agent 使用 SLM(小模型)时,「让 SLM 编排,让工具干活」意味着什么?

SLM 的强项是结构化、有限的任务,比如选择调用哪个函数、填什么参数,而弱项是开放式的多跳推理和广博的世界知识。因此设计思路是让小模型只做调度器,把真正需要知识和数据的工作交给读文件、搜文档等工具。角色一旦倒置,小模型要么被迫编造答案,要么在复杂推理上给出不可靠的结论。

2. 下列哪类场景是必须把 Agent 跑在本地的典型理由?

本地化的核心理由是隐私、成本和离线可用性,医疗病历、机密合同、车间离线巡检都属此类。追求最强推理能力恰恰是云端大模型的优势,本地小模型在这类任务上反而更弱。云端服务成本高企时,本地模型也能省下 token 费用,但这不是唯一理由。

3. 即使 Agent 完全跑在本地,为什么工具(如 read_file)仍要做路径沙箱校验?

「本地」只意味着数据不出机器,并不意味着模型的行为可信。模型可能因为提示注入或自身错误去请求项目外的敏感路径,因此工具要把路径强制限制在项目根目录内,本质上是零信任原则的落地。放宽路径限制恰恰是危险来源,与权限管理的方向完全相反。