ai-agents-for-beginners 如何在 Foundry Local 上用 Qwen 运行完全离线的工具调用智能体
【免费下载链接】ai-agents-for-beginners18 Lessons to Get Started Building AI Agents项目地址: https://gitcode.com/GitHub_Trending/ai/ai-agents-for-beginners
这篇文章解决一个具体问题:把原本指向云端的 Agent 代码迁移到一台开发机上,让智能体在零云端推理调用的前提下完成工具调用(读取文件、分析代码、检索本地文档)。项目ai-agents-for-beginners的 Lesson 17 提供了完整的实现路径:用 Microsoft Foundry Local 在本地托管 Qwen 函数调用模型,再通过 OpenAI 兼容端点接入智能体代码。适用前提是有一台开发工作站(文档给出8 GB RAM 是现实最低值,16 GB 以上更从容;GPU 或 NPU 有帮助但非必需,Foundry Local 会自动选择 CPU/GPU/NPU 构建)、Python 3.12+,以及已安装的 Microsoft Foundry Local。
准备条件
- 安装 Microsoft Foundry Local。README 给出的平台示例命令为(按你操作系统的官方文档选择对应方式安装):
# Windows winget install Microsoft.FoundryLocal # macOS brew install microsoft/foundrylocal/foundrylocal- 安装 Python 依赖。Lesson 17 需要仓库根目录
requirements.txt中的包,外加本课的foundry-local-sdk、openai和chromadb。仓库的 requirements.txt 已包含foundry-local-sdk和chromadb(Lesson 17 小节),以及openai>=1.108.1。Notebook 内的安装命令为:
%pip install foundry-local-sdk openai chromadb -q下载并启动本地 Qwen 模型
Foundry Local 负责下载、管理并在本机提供模型服务。README 与 notebook 给出的启动命令一致:
# 下载并运行 Qwen 模型,然后启动本地服务 foundry model run qwen2.5-7b-instruct foundry service status用foundry service status确认服务处于运行状态。服务跑起来后,你会得到一个本地 OpenAI 兼容端点(形如http://localhost:PORT/v1)。注意本文使用qwen2.5-7b-instruct这个具体模型别名,它被文档选中的原因是 Qwen 系列经过函数调用训练,能稳定产出格式正确的 tool call——这是"本地聊天模型"变成"本地智能体"的关键,换成不能可靠函数调用的 SLM 这条路径不成立。
用 OpenAI SDK 连接本地端点
迁移的核心只有一处改动:客户端的base_url从云端换成 Foundry Local 的本地端点,API key 使用本地占位值,不涉及任何云凭据。连接代码(来自 notebook):
from foundry_local import FoundryLocalManager from openai import OpenAI MODEL_ALIAS = "qwen2.5-7b-instruct" # FoundryLocalManager 会自动选择适合你硬件的构建(CPU / GPU / NPU) manager = FoundryLocalManager(MODEL_ALIAS) model_info = manager.get_model_info(MODEL_ALIAS) client = OpenAI( base_url=manager.endpoint, # 例如 http://localhost:PORT/v1 api_key=manager.api_key, # 本地占位 key ) MODEL_ID = model_info.id print(f"Connected to Foundry Local. Serving: {MODEL_ID}") print(f"Endpoint: {manager.endpoint}")FoundryLocalManager会自动发现/下载模型并返回本地端点,因此不需要手动硬编码端口。这段代码打印出模型 ID 和端点地址,即表示客户端已连上本地服务。
定义沙箱化的本地工具
工具是普通 Python 函数,但必须限定在单个项目目录内——文档强调即使在本机,能读任意路径的工具也应以当前用户权限触碰一切,沙箱检查不可省略。Notebook 先创建一个自包含的示例项目sample_project(含auth.py、utils.py),然后定义带沙箱检查的工具:
import json from pathlib import Path # 创建一个小型示例项目,保证 notebook 自包含 PROJECT_ROOT = Path.cwd() / "sample_project" PROJECT_ROOT.mkdir(exist_ok=True) def _safe_path(path: str) -> Path | None: """解析路径并确认它仍留在项目沙箱内。""" full = (PROJECT_ROOT / path).resolve() if full == PROJECT_ROOT or PROJECT_ROOT in full.parents: return full return None def list_files() -> str: """列出项目目录中的文件。""" files = [p.name for p in PROJECT_ROOT.iterdir() if p.is_file()] return ", ".join(files) if files else "(no files)" def read_file(path: str) -> str: """只读取沙箱项目目录内的文件。""" full = _safe_path(path) if full is None: return "Access denied: path is outside the project directory." if not full.is_file(): return f"No such file: {path}" return full.read_text(encoding="utf-8") def analyze_code(path: str) -> str: """报告一个源文件的简单指标。""" full = _safe_path(path) if full is None or not full.is_file(): return "File not found or access denied." text = full.read_text(encoding="utf-8") lines = text.splitlines() return json.dumps({ "path": path, "lines": len(lines), "functions": sum(1 for ln in lines if ln.strip().startswith("def ")), "todos": sum(1 for ln in lines if "TODO" in ln or "FIXME" in ln), })本地 RAG 工具使用Chroma:它是嵌入式向量库,进程内运行、向量存盘,无需服务器。Notebook 中用chromadb.Client()的本地默认嵌入模型把几条文档说明嵌入project_docscollection,再用collection.query(query_texts=[query], n_results=2)实现search_docs检索。整条链路(嵌入模型、向量库、检索、SLM)全部在本机。
注册工具并运行工具调用循环
用 OpenAI 的 tools schema 把四个工具(list_files、read_file、analyze_code、search_docs)注册给模型,然后运行标准工具调用循环:模型请求工具 → 本地执行 → 结果回填 → 循环直到模型给出最终回答,最多 5 轮:
TOOLS_SCHEMA = [ {"type": "function", "function": { "name": "list_files", "description": "List files in the project directory.", "parameters": {"type": "object", "properties": {}}, }}, {"type": "function", "function": { "name": "read_file", "description": "Read a file inside the project directory.", "parameters": {"type": "object", "properties": { "path": {"type": "string", "description": "File name, e.g. auth.py"}}, "required": ["path"]}, }}, {"type": "function", "function": { "name": "analyze_code", "description": "Report line count, function count and TODO count for a file.", "parameters": {"type": "object", "properties": { "path": {"type": "string"}}, "required": ["path"]}, }}, {"type": "function", "function": { "name": "search_docs", "description": "Search local documentation for a query.", "parameters": {"type": "object", "properties": { "query": {"type": "string"}}, "required": ["query"]}, }}, ] TOOL_IMPL = { "list_files": list_files, "read_file": read_file, "analyze_code": analyze_code, "search_docs": search_docs, } SYSTEM_PROMPT = ( "You are a local engineering assistant. Use the provided tools to inspect the project " "and its documentation. Prefer calling a tool over guessing. Be concise." ) def run_agent(user_query: str, max_iterations: int = 5) -> str: """标准工具调用循环,完全针对本地模型运行。""" messages = [ {"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": user_query}, ] for _ in range(max_iterations): response = client.chat.completions.create( model=MODEL_ID, messages=messages, tools=TOOLS_SCHEMA, ) msg = response.choices[0].message if not msg.tool_calls: return msg.content or "(no answer)" # 记录助手的工具调用请求 messages.append({ "role": "assistant", "content": msg.content, "tool_calls": [tc.model_dump() for tc in msg.tool_calls], }) # 本地执行每个请求的工具并把结果回填 for tc in msg.tool_calls: name = tc.function.name args = json.loads(tc.function.arguments or "{}") result = TOOL_IMPLname if name in TOOL_IMPL else f"Unknown tool: {name}" messages.append({ "role": "tool", "tool_call_id": tc.id, "content": str(result), }) return "Stopped: reached max tool-calling iterations."运行并验证结果
Notebook 用三个问题验证智能体,分别覆盖文件读取、RAG 检索和代码分析三类工具路径:
# 文件读取类问题 print(run_agent("What does auth.py do, and is there anything to fix in it?")) # RAG 类问题 print(run_agent("According to the docs, how are passwords currently handled?")) # 代码分析类问题 print(run_agent("How many functions and TODOs are in auth.py?"))判断依据按文档实际展示的内容核对:
- 连接阶段打印
Connected to Foundry Local. Serving: {MODEL_ID}与端点地址,说明客户端连上了本地服务(端口因机器而异,以实际打印为准)。 - 循环定义完成后打印
Agent ready.,表示工具已注册、智能体可运行。 - 三类问题各自返回模型基于本地工具结果生成的回答。示例项目
auth.py里预置了明文密码比较和一条TODO注释,因此文件读取类问题应能指出密码未哈希的问题,分析类问题应统计出TODO数——但具体措辞由模型生成,不要把某一次运行结果当成固定预期。 - 若超过 5 轮仍无最终回答,
run_agent返回Stopped: reached max tool-calling iterations.,这是循环的终止保护而非报错。
文档说明该流程"在任意时刻都不使用云端推理",网络边界内的组件只有:Foundry Local 服务、Chroma 向量库和工具函数本身。
可选分支:接入本地 MCP 服务器
MCP 是传输协议而非云服务,MCP 服务器可以作为stdio本地进程运行。Notebook 用环境变量LOCAL_MCP_COMMAND控制这一段:未设置时打印提示并跳过,notebook 仍可端到端跑完;设置了本地 MCP 服务器命令(文档示例为 filesystem 服务器指向项目目录)后,代码会初始化ClientSession并列出其工具。这一节是可选的,不影响前面主路径。
限制与边界
- 能力边界:文档明确 SLM 擅长有界任务与工具选择,弱于开放式多跳推理和广博世界知识。本方案的设计策略是"让 SLM 编排、让工具干重活",不要指望本地 7B 模型替代云端前沿模型做深度推理。
- 工具权限:工具以你的用户权限运行,
PROJECT_ROOT沙箱检查(_safe_path)和 MCP 服务器的目录范围限制都要保留,输出应视为需要校验的输入。 - 性能预期:文档只给出 8 GB 是现实最低内存值、更多内存可跑更大模型和更多上下文,没有给出具体延迟数值,不要按固定性能预期规划。
下一步
Lesson 17 README 的 Assignment 给出了延伸方向:把本地工程助手扩展成"本地文档审查器"(索引至少 5 个文件、新增find_todos工具、用三个组合问题并计时验证),以及讨论哪些能力留本地、哪些移到云端。课程的下一课(Lesson 18,见 18-securing-ai-agents/README.md)转向部署后智能体的安全加固。
【免费下载链接】ai-agents-for-beginners18 Lessons to Get Started Building AI Agents项目地址: https://gitcode.com/GitHub_Trending/ai/ai-agents-for-beginners
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考