从零构建本地AI Agent:基于LangChain与Ollama的实践指南
1. 项目概述:从概念到可运行的本地AI Agent
最近和不少同行聊起AI Agent,发现一个挺有意思的现象:大家讨论起Agent的架构、潜力、未来生态都头头是道,但当我问“你自己动手跑起来过一个最简单的本地Agent吗?”,很多人就沉默了。这让我想起早些年学编程,看再多设计模式,也不如亲手写个“Hello World”来得实在。今天,我就想抛开那些宏大的叙事,聚焦一个最朴素的问题: 一个能真正在你本地电脑(无论是Windows、macOS还是Linux)上跑起来的AI Agent,它到底是怎么一步步构建并运转起来的?
这个过程,远不止是调用一个API那么简单。它涉及到如何让一个“大脑”(大语言模型)在本地安家落户,如何为它配备“感官”和“手脚”(工具系统),以及如何设计一套“神经系统”(Agentic Loop)来协调它的思考与行动。我们将要搭建的,是一个具备基础自主能力的智能体原型,它能够理解你的自然语言指令,调用你赋予它的工具(比如查询天气、读写本地文件、进行数学计算),并通过一个循环决策过程完成任务。这不仅是理解AI Agent核心机制的最佳实践,更是你迈向自主开发更复杂Agent的坚实第一步。
适合阅读这篇内容的你,可能是对AI应用开发感兴趣的工程师,希望将大模型能力集成到本地工作流中的效率达人,或者是任何厌倦了云端延迟和隐私顾虑,想要完全掌控自己AI助手的实践者。我会假设你具备基础的编程知识(熟悉Python更佳),但对Agent开发是零起点。我们将从最根本的环境搭建开始,用尽可能清晰的代码和类比,揭开本地AI Agent运行的神秘面纱。
2. 核心架构拆解:理解AI Agent的“五脏六腑”
在动手写代码之前,我们必须先在心里搭好蓝图。一个能够自主运行的AI Agent,其核心架构可以类比为一个具备感知、思考、行动和反思能力的智能生物。下面这张图清晰地描绘了它的核心组件与工作流:
flowchart TD
A[用户输入<br>自然语言指令] --> B[推理引擎<br>(LLM核心)]
B --> C{决策分析}
C -- “需要工具” --> D[工具系统<br>(技能库)]
D --> E[工具执行<br>(代码/API调用)]
E --> F[观察结果]
F --> B
C -- “直接回答” --> G[生成自然语言响应]
G --> H[输出最终结果]
F --> B
subgraph [学习与记忆层(可选)]
I[短期记忆<br>(对话历史)]
J[长期记忆<br>(向量数据库)]
end
I --> B
J --> B
接下来,我们逐一拆解图中的每一个关键部分。
2.1 大脑:本地大语言模型(LLM)的选型与部署
Agent的“大脑”就是大语言模型。在本地运行,意味着我们需要一个能在自己计算机上离线推理的模型。这通常不是指ChatGPT或Claude的API,而是如Llama 3、Qwen、DeepSeek等开源模型。
选型考量:
- 模型尺寸与硬件平衡 :模型参数量(如7B、13B、70B)直接决定了对GPU显存或系统内存的需求。一个经验法则是,量化后的7B模型可能在8GB内存的电脑上勉强运行,而13B模型则需要16GB以上。对于入门,我强烈推荐从量化后的7B或更小的模型开始,例如
Qwen2.5-7B-Instruct或Llama-3.2-3B,它们对硬件友好且能力足够用于演示核心逻辑。 - 推理后端 :你需要一个软件来加载和运行模型。目前最流行的选择是 Ollama 。它就像是一个本地化的模型容器和管理器,通过简单的命令行就能拉取、运行和与上百种模型交互,极大降低了部署门槛。另一个选择是 LM Studio ,它提供了图形界面,对新手更友好。
- 量化与精度 :为了在有限资源下运行大模型,量化技术将模型权重从高精度(如FP16)转换为低精度(如INT4、INT8),从而大幅减少内存占用,代价是轻微的性能损失。对于本地Agent,使用
Q4_K_M或Q5_K_M这类量化等级通常能在性能和精度间取得很好的平衡。
实操部署(以Ollama为例):
# 1. 安装Ollama(访问官网获取对应系统安装包)
# 2. 拉取并运行一个量化模型,例如Qwen2.5
ollama run qwen2.5:7b
# 首次运行会自动下载模型,之后就会进入一个交互式聊天界面
这步成功后,你的“大脑”就已经在本地待命了。Ollama会在本地启动一个API服务(默认在11434端口),我们的Agent程序将通过这个API与模型“大脑”对话。
注意 :模型首次下载可能需要较长时间和大量磁盘空间(几个GB)。确保你的网络环境稳定,并有足够的存储空间。
2.2 感官与手脚:工具系统的设计与集成
一个只有大脑的Agent是“瘫痪”的,它需要工具(Tools)作为其感知和影响外部世界的接口。工具本质上是一个个函数,它们能被LLM调用,并返回执行结果。
工具设计原则:
- 明确的描述 :每个工具都必须有一个清晰的名字和功能描述,LLM依靠这些描述来决定何时调用哪个工具。描述要具体,例如“获取指定城市的当前天气和气温”,而不是模糊的“查天气”。
- 结构化的输入/输出 :工具的参数应该被明确定义为JSON Schema,这样LLM才能生成正确的调用格式。输出也最好是结构化的数据,便于LLM解析。
- 安全性 :本地Agent可能被授予访问文件系统、执行命令的权限。必须严格控制工具的能力范围,避免执行危险操作(如
rm -rf /)。一种常见做法是使用沙箱环境或进行严格的输入校验。
一个简单的工具示例(Python):
import requests
from datetime import datetime
def get_current_time(location: str = “”):
“””
获取当前时间。
参数:
location (str): 城市名(仅为上下文提示,实际返回系统时间)。例如:“北京”。
返回:
str: 格式化的当前时间字符串。
“””
current_time = datetime.now().strftime(“%Y-%m-%d %H:%M:%S”)
return f”当前系统时间({location if location else ‘本地’})是:{current_time}”
def search_web(query: str):
“””
使用DuckDuckGo即时答案进行网络搜索(模拟,实际需安装库)。
参数:
query (str): 搜索查询词。
返回:
str: 搜索结果的摘要。
“””
# 此处为简化示例,实际可使用duckduckgo-search等库
return f”关于‘{query}’的模拟搜索结果:这是一个演示,真实工具需要接入搜索API。”
在Agent框架中,我们需要将这些函数及其描述注册到工具库中,供LLM在推理时查阅和调用。
2.3 神经系统:Agentic Loop(智能体循环)的工作流
这是Agent的“灵魂”,是协调大脑思考、决策和行动的核心循环机制。一个典型的简化循环如下:
- 规划(Plan) :LLM根据用户指令和当前上下文,思考需要达成目标的步骤。它可能会说:“用户想了解今天的天气并记录到文件。我需要先调用天气查询工具,然后调用文件写入工具。”
- 行动(Act) :LLM根据规划,决定调用哪个工具,并生成符合工具参数格式的调用指令(如JSON)。
- 观察(Observe) :工具执行完毕,将结果(成功或失败)返回给LLM。
- 反思(Reflect) :LLM观察工具执行结果,评估当前目标完成情况。如果未完成(例如,工具调用失败,或结果不完整),则进入下一轮循环,重新规划或调整行动。
这个“规划 -> 行动 -> 观察 -> 反思”的循环会持续进行,直到LLM认为任务已达成,或达到最大循环次数限制。这个过程确保了Agent能够处理复杂、多步骤的任务,而不仅仅是单轮问答。
2.4 记忆与状态管理:让Agent拥有“上下文”
短期记忆(对话历史)是让Agent保持连贯性的关键。我们需要在每次与LLM交互时,将之前的对话历史、工具调用及结果作为上下文(Context)一并送入模型。这通常通过维护一个消息列表来实现,列表中包含 system (系统指令)、 user (用户输入)、 assistant (AI回复)、 tool (工具调用及结果)等不同角色的消息。
长期记忆(如向量数据库)对于需要记住大量历史信息或知识的Agent是进阶能力。它允许Agent将信息嵌入成向量存储起来,并在需要时进行语义检索。对于我们的第一个本地Agent,可以先聚焦于短期记忆的实现。
3. 从零搭建:一个极简本地AI Agent的实现
理论说得再多,不如一行代码。让我们使用Python和流行的 LangChain 框架来构建一个最小可运行的Agent。LangChain提供了丰富的抽象,能让我们更关注逻辑而非底层通信。
3.1 环境准备与依赖安装
首先,确保你的Python环境(建议3.9以上)并安装必要库。我们将使用LangChain来编排Agent,并使用Ollama作为本地LLM后端。
# 创建并进入项目目录
mkdir local_ai_agent && cd local_ai_agent
python -m venv venv # 创建虚拟环境
# 激活虚拟环境
# Windows: venv\Scripts\activate
# macOS/Linux: source venv/bin/activate
# 安装核心依赖
pip install langchain langchain-community langchain-core
# 安装用于连接Ollama的库
pip install ollama
# 安装可能用到的工具依赖(示例)
pip install duckduckgo-search # 用于真实网络搜索
3.2 构建核心组件:模型、工具与提示词
第一步:连接本地LLM(Ollama)
# main.py
from langchain_community.llms import Ollama
from langchain_core.prompts import ChatPromptTemplate
# 初始化连接本地Ollama服务的LLM
# 确保你已经通过 `ollama run qwen2.5:7b` 让模型在后台运行
llm = Ollama(model=“qwen2.5:7b”, base_url=“http://localhost:11434”)
# 测试连接
print(llm.invoke(“你好,请用中文回复。”))
第二步:定义我们的工具集 我们将创建两个工具:一个获取时间,一个进行网络搜索(模拟)。
from langchain.tools import tool
from datetime import datetime
import requests
@tool
def get_current_time(location: str = “”) -> str:
“””获取指定地点(或本地)的当前时间。location参数仅为上下文提供。”””
current_time = datetime.now().strftime(“%Y-%m-%d %H:%M:%S”)
return f”当前系统时间({location if location else ‘本地’})是:{current_time}”
@tool
def search_web(query: str) -> str:
“””使用DuckDuckGo搜索网络信息。请提供一个明确的搜索查询词。”””
try:
from duckduckgo_search import DDGS
with DDGS() as ddgs:
results = list(ddgs.text(query, max_results=3))
if results:
summary = “\n”.join([f”{r[‘title’]}: {r[‘body’]}” for r in results[:2]])
return f”搜索 ‘{query}’ 的结果摘要:\n{summary}”
else:
return f”未找到关于 ‘{query}’ 的相关结果。”
except ImportError:
return “错误:未安装duckduckgo-search库,请运行 ‘pip install duckduckgo-search’。模拟结果:这是一个关于 ‘{query}’ 的示例搜索结果。”
# 将工具放入列表
tools = [get_current_time, search_web]
第三步:设计系统提示词(System Prompt) 提示词是指导LLM扮演Agent角色的关键。它定义了Agent的身份、行为准则和工具使用规范。
system_prompt = “””你是一个运行在用户本地电脑上的AI助手。你的目标是准确理解用户需求,并利用可用的工具来完成任务。
你可以使用的工具如下:
{tools}
请严格遵守以下规则:
1. 当用户的问题需要借助工具才能回答时,你必须选择调用最合适的工具。
2. 调用工具时,请严格按照工具描述的格式提供参数。
3. 工具返回结果后,你需要对结果进行总结和解释,用友好、自然的中文回复用户。
4. 如果工具调用失败或结果不理想,你可以尝试分析原因,或告知用户。
5. 如果不需要工具就能直接回答,请直接给出答案。
当前对话历史:
{chat_history}
用户问题:{input}
请开始思考并行动:”””
3.3 组装Agent并实现核心循环
现在,我们将上述组件组装起来,并手动实现一个简化的Agentic Loop,以便更清晰地理解其流程。
from langchain.schema import AIMessage, HumanMessage, SystemMessage
import json
class SimpleLocalAgent:
def __init__(self, llm, tools, system_prompt):
self.llm = llm
self.tools = {tool.name: tool for tool in tools}
self.system_prompt_template = system_prompt
self.chat_history = [] # 用于存储对话历史
def _format_tools_description(self):
“””将工具列表格式化为字符串描述,用于放入提示词。”””
desc = []
for tool in self.tools.values():
desc.append(f”- {tool.name}: {tool.description} 参数: {tool.args}”)
return “\n”.join(desc)
def run(self, user_input: str, max_turns: int = 5):
print(f“\n[用户] {user_input}”)
full_conversation = []
for turn in range(max_turns):
# 1. 准备当前轮次的提示词
tools_desc = self._format_tools_description()
prompt = ChatPromptTemplate.from_template(self.system_prompt_template)
formatted_prompt = prompt.format(
tools=tools_desc,
chat_history=“\n”.join([f”{msg[‘role’]}: {msg[‘content’]}” for msg in self.chat_history[-6:]]), # 保留最近几轮历史
input=user_input if turn == 0 else “继续处理,直到任务完成或无法进行。”
)
# 2. 调用LLM进行“思考”
llm_response = self.llm.invoke(formatted_prompt)
print(f“[AI思考] {llm_response[:200]}...”) # 打印部分思考过程
# 3. 解析LLM响应,判断是直接回答还是调用工具
# 这里是一个简化的解析逻辑。在实际框架中,这部分由更复杂的输出解析器完成。
response_text = llm_response.strip()
# 判断逻辑:如果响应中包含类似“调用工具XXX”的文本,则尝试解析
if “调用工具” in response_text or “Action:” in response_text: # 简单关键词匹配
# 尝试提取工具名和参数(这是一个非常简单的示例,实际应用需要更鲁棒的解析)
lines = response_text.split(‘\n’)
tool_to_use = None
tool_input = {}
for line in lines:
if “工具名” in line or “Tool:” in line:
potential_name = line.split(‘:’)[1].strip()
if potential_name in self.tools:
tool_to_use = self.tools[potential_name]
elif “参数” in line or “Input:” in line:
# 假设参数是JSON字符串
import re
json_match = re.search(r‘\{.*\}’, line)
if json_match:
try:
tool_input = json.loads(json_match.group())
except:
tool_input = {“query”: line.split(‘:’)[1].strip()}
if tool_to_use:
print(f“[行动] 调用工具: {tool_to_use.name}, 参数: {tool_input}”)
# 4. 执行工具
try:
tool_result = tool_to_use.invoke(tool_input)
print(f“[观察] 工具结果: {tool_result}”)
# 将工具执行结果作为下一轮LLM的输入
user_input = f”工具 ‘{tool_to_use.name}’ 的执行结果是:{tool_result}。请根据这个结果继续回答用户最初的问题。”
# 记录到历史
self.chat_history.extend([
{“role”: “assistant”, “content”: f”我调用了工具 {tool_to_use.name}。”},
{“role”: “tool”, “content”: str(tool_result)}
])
continue # 进入下一轮循环
except Exception as e:
error_msg = f”工具执行失败: {str(e)}”
print(f”[观察] {error_msg}”)
user_input = error_msg
continue
else:
# 解析失败,作为普通响应输出
final_answer = response_text
break
else:
# LLM决定直接回答
final_answer = response_text
break
else:
# 循环达到最大次数仍未结束
final_answer = “任务处理已达到最大步数,可能尚未完全解决。建议您简化问题或重试。”
# 记录最终回答到历史
self.chat_history.append({“role”: “assistant”, “content”: final_answer})
print(f“[最终回答] {final_answer}”)
return final_answer
# 初始化并运行Agent
if __name__ == “__main__”:
agent = SimpleLocalAgent(llm, tools, system_prompt)
# 示例交互
agent.run(“现在北京的时间是几点?”)
agent.run(“帮我搜索一下LangChain的最新版本信息。”)
这个 SimpleLocalAgent 类实现了一个最基础的循环。它接收用户输入,生成提示词给LLM,尝试解析LLM的响应以判断是否需要调用工具,如果需要则调用并观察结果,然后将结果作为新的上下文输入给LLM,进入下一轮,直到LLM给出最终答案或达到循环上限。
实操心得 :在解析LLM响应以决定是否调用工具时,上述简单关键词匹配的方法非常脆弱。在生产环境中,强烈建议使用LangChain内置的
AgentExecutor或强制LLM使用结构化输出(如JSON格式)来声明其“动作”,这会稳定可靠得多。这里的简化实现是为了让你看清循环的本质。
3.4 使用LangChain的AgentExecutor(推荐实践)
手动实现循环有助于理解,但LangChain提供了更强大、更稳定的 AgentExecutor ,它能处理复杂的解析、错误和流式输出。下面是如何用更标准的方式构建同一个Agent:
from langchain.agents import create_react_agent, AgentExecutor
from langchain import hub
from langchain.agents.output_parsers import ReActSingleInputOutputParser
from langchain.tools.render import render_text_description
# 1. 拉取一个优化的ReAct提示词模板(LangChain Hub上有许多)
prompt = hub.pull(“hwchase17/react-chat”)
# 根据我们的工具和系统提示稍作修改
prompt = prompt.partial(
tools=render_text_description(tools),
tool_names=“, “.join([t.name for t in tools]),
)
# 2. 创建ReAct Agent
agent = create_react_agent(llm, tools, prompt)
# 3. 创建Agent执行器
agent_executor = AgentExecutor(
agent=agent,
tools=tools,
verbose=True, # 打印详细的执行步骤,便于调试
handle_parsing_errors=True, # 优雅处理解析错误
max_iterations=5, # 限制最大循环次数,防止死循环
early_stopping_method=“generate”, # 停止条件
)
# 4. 运行Agent
try:
result = agent_executor.invoke({
“input”: “先获取当前时间,然后搜索一下今天纽约的天气新闻。”,
“chat_history”: [] # 可以传入历史消息
})
print(“\n=== 最终输出 ===”)
print(result[“output”])
except Exception as e:
print(f“Agent执行出错: {e}”)
当设置 verbose=True 时,你会在终端看到类似以下的详细日志,这正是Agentic Loop的生动体现:
> Entering new AgentExecutor chain...
思考:用户需要我先获取时间,再搜索新闻。我有`get_current_time`和`search_web`两个工具。
行动:调用`get_current_time`工具。
Action: get_current_time
Action Input: {}
观察:当前系统时间(本地)是:2024-05-27 14:30:15
思考:我已经得到了时间。现在需要搜索纽约的天气新闻。调用`search_web`工具。
Action: search_web
Action Input: {“query”: “纽约 天气 新闻 2024年5月27日”}
观察:搜索 ‘纽约 天气 新闻 2024年5月27日’ 的结果摘要:...
思考:我获得了时间和搜索摘要,现在可以综合回答用户了。
最终回答:当前时间是2024-05-27 14:30:15。根据搜索,今天纽约的天气情况是...
> Finished chain.
使用 AgentExecutor ,我们无需手动解析LLM输出和处理循环逻辑,框架已经为我们封装好了健壮的ReAct(Reasoning + Acting)模式。这是构建生产级Agent的起点。
4. 进阶与优化:让你的本地Agent更强大
一个能跑起来的基础Agent只是起点。要让它在实际场景中真正有用,我们需要从以下几个方面进行增强。
4.1 工具系统的扩展与实践
基础工具只能满足简单需求。一个实用的本地Agent应该能与你电脑上的其他应用和数据交互。
1. 文件系统工具:
import os
from pathlib import Path
from langchain.tools import tool
@tool
def read_file(file_path: str) -> str:
“””读取指定路径的文本文件内容。”””
path = Path(file_path)
if not path.exists():
return f”错误:文件 ‘{file_path}’ 不存在。”
if not path.is_file():
return f”错误:’{file_path}’ 不是一个文件。”
try:
return path.read_text(encoding=‘utf-8’)
except Exception as e:
return f”读取文件失败: {str(e)}”
@tool
def write_file(file_path: str, content: str) -> str:
“””将内容写入指定路径的文件。如果文件存在则覆盖。”””
path = Path(file_path)
try:
path.parent.mkdir(parents=True, exist_ok=True) # 确保目录存在
path.write_text(content, encoding=‘utf-8’)
return f”成功写入文件 ‘{file_path}’。”
except Exception as e:
return f”写入文件失败: {str(e)}”
@tool
def list_directory(dir_path: str = “.”) -> str:
“””列出指定目录下的文件和子目录。”””
path = Path(dir_path)
if not path.exists():
return f”错误:目录 ‘{dir_path}’ 不存在。”
if not path.is_dir():
return f”错误:’{dir_path}’ 不是一个目录。”
items = []
for item in path.iterdir():
items.append(f”[{'DIR' if item.is_dir() else 'FILE'}] {item.name}”)
return “\n”.join(items) if items else “目录为空。”
2. 系统信息与计算工具:
import psutil
import math
@tool
def get_system_info() -> str:
“””获取当前系统的CPU、内存使用情况。”””
cpu_percent = psutil.cpu_percent(interval=1)
memory = psutil.virtual_memory()
return f”CPU使用率: {cpu_percent}%\n内存使用: {memory.used / (1024**3):.2f} GB / {memory.total / (1024**3):.2f} GB ({memory.percent}%)”
@tool
def calculate(expression: str) -> str:
“””计算一个数学表达式。支持加减乘除(+-*/)、乘方(**)、括号。例如:’(3+4)*2’。”””
# 警告:使用eval有安全风险,仅限在受控环境中用于演示。
# 生产环境应使用更安全的表达式解析库(如ast.literal_eval,但功能有限)。
try:
# 非常基础的安全检查(极其简陋,切勿用于生产!)
allowed_chars = set(“0123456789+-*/(). ** ”)
if not all(c in allowed_chars for c in expression):
return “错误:表达式包含不安全字符。”
result = eval(expression)
return f”{expression} = {result}”
except Exception as e:
return f”计算错误: {str(e)}”
3. 集成外部API工具(以天气为例):
import os
from langchain.tools import tool
import requests
@tool
def get_weather(city: str) -> str:
“””获取指定城市的当前天气情况。需要配置API密钥。”””
api_key = os.getenv(“WEATHER_API_KEY”) # 从环境变量读取密钥
if not api_key:
return “错误:未配置天气API密钥。请设置环境变量 WEATHER_API_KEY。”
base_url = “http://api.weatherapi.com/v1/current.json"
params = {
“key”: api_key,
“q”: city,
“aqi”: “no”
}
try:
response = requests.get(base_url, params=params, timeout=10)
data = response.json()
if “current” in data:
current = data[“current”]
location = data[“location”]
return f”{location[‘name’]}的天气:{current[‘condition’][‘text’]},温度{current[‘temp_c’]}°C,湿度{current[‘humidity’]}%,风速{current[‘wind_kph’]}km/h。”
else:
return f”获取天气失败: {data.get(‘error’, {}).get(‘message’, ‘未知错误’)}”
except Exception as e:
return f”请求天气API时出错: {str(e)}”
将这些新工具添加到之前的 tools 列表中,你的Agent立刻就拥有了与本地文件系统交互、监控系统状态、进行数学计算甚至查询实时天气的能力。工具的扩展性是Agent能力边界拓展的核心。
重要安全警告 :给Agent赋予文件读写、系统访问乃至
eval计算的能力是 极其危险 的。在开放环境中,必须实施严格的权限控制、输入验证和沙箱机制。例如,可以将文件操作限制在特定沙箱目录内,对数学表达式使用安全的解析库(如numexpr),并永远不要允许Agent执行任意Shell命令。对于个人本地使用,也务必保持警惕。
4.2 记忆机制的实现:短期与长期记忆
基础循环只维护了简单的对话历史。更复杂的记忆系统能让Agent在长对话中保持一致性和连贯性。
增强短期记忆(对话历史管理): LangChain的 AgentExecutor 已经自动管理了对话历史。但我们可以定制历史窗口的长度和格式。
from langchain.memory import ConversationBufferWindowMemory
memory = ConversationBufferWindowMemory(
memory_key=“chat_history”, # 存储在输入字典中的键名
k=5, # 保留最近5轮对话
return_messages=True # 以消息对象格式返回,而非字符串
)
# 在创建AgentExecutor时传入memory
agent_executor = AgentExecutor(
agent=agent,
tools=tools,
memory=memory,
verbose=True,
max_iterations=5,
)
现在,每次调用 agent_executor.invoke 时,它会自动将当前的输入输出添加到记忆里,并在下一次调用时将最近的对话历史作为上下文的一部分提供给LLM。
引入长期记忆(向量数据库): 当Agent需要记住大量文档、笔记或历史对话细节时,就需要向量数据库。这里以ChromaDB为例,展示如何让Agent“记住”你提供的文档内容。
from langchain_community.document_loaders import TextLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_community.vectorstores import Chroma
from langchain_community.embeddings import OllamaEmbeddings # 使用本地Ollama生成嵌入
# 1. 加载文档(例如,你的个人笔记)
loader = TextLoader(“my_notes.txt”, encoding=“utf-8”)
documents = loader.load()
# 2. 分割文本
text_splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=50)
texts = text_splitter.split_documents(documents)
# 3. 创建向量存储(使用本地Ollama的嵌入模型)
embeddings = OllamaEmbeddings(model=“nomic-embed-text”, base_url=“http://localhost:11434”)
vectorstore = Chroma.from_documents(documents=texts, embedding=embeddings, persist_directory=“./chroma_db”)
vectorstore.persist() # 持久化到磁盘
# 4. 将向量存储转换为检索工具(Retriever)
retriever = vectorstore.as_retriever(search_kwargs={“k”: 3}) # 检索最相关的3个片段
# 5. 创建一个基于检索结果的问答工具
from langchain.tools.retriever import create_retriever_tool
retriever_tool = create_retriever_tool(
retriever,
“search_personal_notes”,
“在用户的个人笔记和文档中搜索相关信息。当用户问及关于个人计划、项目细节或已记录的信息时使用此工具。”
)
# 6. 将这个新工具加入到Agent的工具列表中
tools.append(retriever_tool)
现在,当你问Agent“我上个月提到的那个项目截止日期是什么时候?”,它就会使用 search_personal_notes 工具,从你的 my_notes.txt 文件中检索相关信息,并将检索到的片段作为上下文来生成答案。这就实现了基于个人知识的长期记忆。
4.3 流式输出与事件驱动
在Web应用或GUI中,我们往往希望看到Agent的思考过程是逐步呈现的,而不是等待所有循环结束后一次性输出。这就是流式输出(Streaming)和事件驱动。
利用LangChain的流式支持: AgentExecutor 支持通过 astream 或 astream_events 方法进行流式输出。
async def run_agent_streaming(query: str):
“””异步流式运行Agent,实时获取每一步的思考、行动和观察。”””
async for event in agent_executor.astream_events({“input”: query}, version=“v1”):
kind = event[“event”]
if kind == “on_chat_model_stream”: # LLM正在生成文本
content = event[“data”][“chunk”].content
if content: # 过滤空内容
print(content, end=“”, flush=True) # 逐词打印
elif kind == “on_tool_start”: # 开始调用工具
print(f”\n[行动] 调用工具: {event[‘name’]}”)
elif kind == “on_tool_end”: # 工具调用结束
print(f”\n[观察] 工具结果: {event[‘output’][:100]}...”) # 打印部分结果
print() # 最终换行
# 在异步环境中调用,例如在Jupyter notebook或FastAPI后端中
# import asyncio
# asyncio.run(run_agent_streaming(“查询北京天气并总结”))
通过流式事件,前端界面可以实时展示Agent的“内心独白”(思考过程)、工具调用动作和工具返回结果,极大地提升了交互体验和可观测性。
4.4 性能优化与稳定性提升
本地运行Agent,性能是关键。以下是一些优化技巧:
- 模型量化与选择 :始终使用量化模型(如GGUF格式的Q4_K_M)。对于纯文本推理,7B-13B的模型通常足够。如果追求更低延迟,可以尝试更小的模型如
Phi-3-mini。 - 上下文长度管理 :对话历史会不断增长,消耗大量上下文窗口(Token)。使用
ConversationSummaryMemory或ConversationBufferWindowMemory来限制历史长度,或定期将长历史总结成摘要。 - 工具调用优化 :避免在单个循环中调用多个耗时工具。对于可并行操作,可以考虑让Agent生成包含多个工具调用的计划,然后在后端并行执行(但这需要更复杂的Agent设计)。
- 设置超时与重试 :为LLM调用和工具调用设置合理的超时时间,并实现简单的重试逻辑,以应对偶发的网络波动或模型加载问题。
- 使用更高效的解析器 :确保使用正确的
output_parser(如ReActSingleInputOutputParser),并考虑让LLM输出严格的JSON格式,这比解析自由文本要可靠和快速得多。
5. 常见问题排查与实战心得
在本地搭建和运行Agent的过程中,你几乎一定会遇到下面这些问题。这里是我踩过坑后总结的排查清单和心得。
5.1 模型加载与连接问题
问题: 运行代码时提示 Connection refused 或 Model not found 。
- 检查Ollama服务 :首先在终端运行
ollama list,确认模型已下载。运行ollama serve确保服务在运行。默认端口是11434,检查是否有其他进程占用。 - 确认模型名称 :
Ollama(model=“qwen2.5:7b”)中的模型名必须与Ollama中的完全一致。使用ollama list查看准确的名称。 - 防火墙/网络 :确保Python脚本能访问
localhost:11434。在某些Docker或虚拟机环境中,localhost可能需要替换为宿主机的IP。
问题: 模型响应速度极慢或内存溢出。
- 检查资源占用 :使用任务管理器(Windows)或
htop(Linux/macOS)查看CPU和内存使用情况。本地推理非常消耗资源。 - 降低量化等级或换更小模型 :如果使用
7b模型内存不足,尝试3b或更小的模型,或者使用更低精度的量化(如q4_0代替q4_k_m)。 - 关闭无关程序 :释放尽可能多的内存给模型。
5.2 Agent逻辑与工具调用问题
问题: Agent不调用工具,总是直接回答。
- 提示词是关键 :系统提示词必须清晰地指示LLM“在需要时使用工具”。参考LangChain Hub上的标准ReAct提示词模板。
- 工具描述要清晰 :工具函数的
docstring(描述)必须准确、无歧义,让LLM明白在什么场景下使用它。 - 启用详细日志 :创建
AgentExecutor时设置verbose=True,观察LLM的完整思考链,看它是否在正确的节点做出了错误的决策。
问题: Agent陷入死循环,不断调用同一个工具。
- 设置
max_iterations:这是最重要的安全阀。在AgentExecutor中务必设置一个合理的最大值(如5-10次)。 - 检查工具输出 :工具是否返回了有意义的结果?如果工具总是返回错误或空结果,LLM可能会因为任务未完成而不断重试。
- 增强反思能力 :在提示词中强调,如果工具连续失败,应停止尝试并向用户报告。
问题: 工具调用参数格式错误。
- 使用结构化工具定义 :确保使用
@tool装饰器或StructuredTool.from_function来定义工具,LangChain会自动为LLM生成JSON Schema。 - 验证LLM输出解析器 :确认使用的是与Agent类型匹配的
output_parser(如ReActSingleInputOutputParser)。
5.3 性能与资源优化心得
- 冷启动慢 :首次加载模型或长时间未调用后第一次推理会很慢。可以考虑写一个简单的守护进程,让模型常驻内存,或者使用
ollama serve并保持运行。 - 上下文切换成本 :如果让Agent处理多个独立会话,为每个会话创建全新的
AgentExecutor实例开销很大。可以考虑复用LLM和工具对象,只重置记忆(memory.clear())。 - 工具耗时阻塞 :如果某个工具执行时间很长(如一个复杂的计算或网络请求),会阻塞整个Agent循环。考虑将工具设计为异步(async),并在异步环境中运行Agent。
- 内存泄漏 :长时间运行后,如果发现内存持续增长,检查是否有全局变量在不断累积数据(如无限增长的对话历史)。合理使用
ConversationBufferWindowMemory限制历史长度。
5.4 安全与隐私提醒(再强调)
- 本地化是双刃剑 :本地运行确实避免了数据上云,但恶意或错误的工具同样能破坏你的本地系统。永远不要赋予Agent不受限制的文件删除、系统命令执行或网络访问权限。
- 环境隔离 :考虑在Docker容器或虚拟机中运行你的Agent实验,尤其是当你打算测试未知工具或模型时。
- 敏感信息 :避免在提示词、工具描述或对话历史中硬编码API密钥、密码等敏感信息。使用环境变量或安全的配置管理工具。
6. 总结与展望:从玩具到工具
走到这里,你已经亲手让一个本地AI Agent“跑起来”了。我们从一个空白的Python环境开始,部署了本地大模型作为大脑,为其装备了获取时间、搜索网络、读写文件等工具,并设计了一个“思考-行动-观察”的循环神经系统来驱动它。通过LangChain框架,我们简化了这个过程,并探讨了如何为其增加记忆、流式交互等进阶能力。
这个最初的Agent可能还是个“玩具”,但它完整地演示了AI Agent最核心的运作原理: 感知(用户输入/工具反馈)、决策(LLM推理)、执行(工具调用)、学习(记忆更新) 。基于这个骨架,你可以无限扩展:
- 更专业的工具 :集成你的代码编辑器(VS Code)、日历(Google Calendar)、邮件客户端,让它成为真正的个人工作流中枢。
- 多Agent协作 :创建多个具有不同专长(写作、分析、代码)的Agent,让它们通过通信协同解决复杂问题。
- 更强大的规划器 :引入Chain-of-Thought(思维链)或Tree-of-Thought(思维树)等高级规划策略,提升复杂任务分解能力。
- 图形化界面 :使用Gradio、Streamlit或Web框架为你的Agent构建一个聊天窗口,方便日常使用。
本地AI Agent的魅力在于,它将最前沿的AI能力从云端巨头的黑盒中解放出来,置于你个人的掌控之下。你可以定制它的性格、扩展它的能力、保障数据的隐私。虽然当前本地模型的性能与顶尖闭源模型尚有差距,但其发展速度日新月异,开源生态也日益繁荣。
我个人的体会是,搭建第一个能跑通的Agent原型,其价值远超阅读十篇架构论文。在这个过程中遇到的每一个错误、每一次调试,都会让你对Agent的“行为模式”和“思维方式”有更直觉的理解。接下来,不妨就以你手头的一个小任务开始——比如,写一个Agent帮你自动整理下载文件夹,或者分析本地日志文件——在实践中去迭代和完善它。当你看到几行简单的指令被自动转化为一系列精准的操作并完成时,那种感觉,正是AI Agent开发最原始的乐趣所在。
更多推荐



所有评论(0)