Day20:打造全能本地轻量Agent,离线运行也能呼风唤雨!

开场白:从“会聊天”到“全能助手”

兄弟们,经过前五天的修炼,我们已经:

  • Day15:学会了LangChain的基础,知道了怎么把提示词、模型、输出串成一条链。

  • Day16:学会了给AI装上工具,让它能查天气、算数学。

  • Day17:给AI装上了记忆,它能记住我们上一句说了啥。

  • Day18:把AI的大脑从云端搬到了本地(Ollama),断网也能跑。

  • Day19:把LangChain和Ollama对接,实现了离线Agent的雏形。

今天,我们要把这些零件全部组装起来,打造一个真正能打、能干活、还能陪你唠嗑的全能本地轻量Agent!它就像一把瑞士军刀,既有工具(天气、时间、计算器、文件操作),又有记忆(不会扭头就忘),还跑得飞快(优化过的)。而且,它完全离线,数据不上传,免费无限使用!

想象一下:你可以让它帮你整理文件、记录笔记、算账、查天气……一切都在你自己的电脑上完成,不用联网,不用花一分钱。是不是很爽?

今天我们就来干这件事!

今日目标:组装一台离线AI瑞士军刀

  • 核心任务:整合本周所有知识,开发一个本地轻量Agent(基于LangChain+Ollama本地模型+自定义Tool+Memory),支持离线运行。

  • 补充任务:优化Agent的运行效率,减少模型推理时间,添加简单的交互界面(命令行交互)。

  • 今日输出:本地轻量Agent代码(可离线运行)。

说白了,我们要做一个能跑在本地、能调用多种工具、有记忆、而且响应很快的命令行AI助手

准备工作:环境检查

确保你已经安装并配置好了以下环境:

  • Python 3.9+(建议使用虚拟环境)

  • Ollama 已安装,并且服务运行中

  • 已拉取至少一个本地模型(如 qwen:7b-q4_0 或 llama3:7b

  • LangChain及相关库:langchainlangchain-communitylangchain-core

快速检查:

ollama list               # 应该看到模型
curl http://localhost:11434   # 应该返回 Ollama is running
pip show langchain        # 应该显示版本

如果缺哪个,赶紧补上。

设计我们的本地轻量Agent

我们将在周五的Agent基础上进行扩展,加入更多实用的工具,优化记忆方式,并提供一个友好的命令行交互界面。

工具库大扩充

之前我们只有天气和计算器,今天我们来添加几个新工具:

  • 时间工具:获取当前时间(可以指定时区、格式)

  • 文件操作工具:读写文件(用于记录笔记、保存数据)

  • 系统命令工具:执行简单的系统命令(比如 lsecho,但要注意安全)

  • 随机数生成器:生成随机数、随机字符串

  • 单位换算:长度、重量、温度换算

我们只演示几个,你可以按需添加。

import datetime
import random
import os
import subprocess
from langchain.tools import tool

@tool
def get_current_time(format: str = "%Y-%m-%d %H:%M:%S") -> str:
    """获取当前时间,可指定格式(例如 '%Y-%m-%d %H:%M:%S')"""
    return datetime.datetime.now().strftime(format)

@tool
def random_number(min_val: int = 0, max_val: int = 100) -> str:
    """生成一个随机整数,范围[min_val, max_val]"""
    return str(random.randint(min_val, max_val))

@tool
def write_note(filename: str, content: str) -> str:
    """将内容写入文件(如果文件不存在则创建),返回写入结果"""
    try:
        with open(filename, 'w', encoding='utf-8') as f:
            f.write(content)
        return f"笔记已保存到 {filename}"
    except Exception as e:
        return f"写入失败:{str(e)}"

@tool
def read_note(filename: str) -> str:
    """读取文件内容"""
    try:
        with open(filename, 'r', encoding='utf-8') as f:
            return f.read()
    except Exception as e:
        return f"读取失败:{str(e)}"

# 注意:执行系统命令有安全风险,这里仅作演示,实际生产环境要谨慎
@tool
def run_command(command: str) -> str:
    """执行一条系统命令(例如 'ls -l'),返回输出结果"""
    try:
        result = subprocess.run(command, shell=True, capture_output=True, text=True, timeout=5)
        return result.stdout if result.stdout else result.stderr
    except Exception as e:
        return f"命令执行失败:{str(e)}"

记忆升级:告别Token浪费

之前我们用的是 ConversationBufferMemory,它会原封不动地保存所有对话历史。随着对话变长,每次调用都会把整个历史塞进提示词,导致token浪费、推理变慢。今天换成 **ConversationSummaryMemory**,它会自动把历史对话总结成摘要,只传递摘要给模型,大大节省上下文长度。

from langchain.memory import ConversationSummaryMemory

memory = ConversationSummaryMemory(
    llm=llm,                    # 需要传入LLM用于生成摘要
    memory_key="chat_history",
    return_messages=True,
    max_token_limit=2000        # 限制摘要的最大token数
)

注意:ConversationSummaryMemory 内部需要调用LLM来生成摘要,所以会额外消耗一点时间,但整体上对于长对话是值得的。我们也可以选择 ConversationBufferWindowMemory 只保留最近N轮对话。

提示词优化:让Agent更聪明

一个好的系统提示词能显著提升Agent的效率和准确性。我们给Agent设定角色,并明确工具使用规则:

from langchain.prompts import ChatPromptTemplate, MessagesPlaceholder

prompt = ChatPromptTemplate.from_messages([
    ("system", """你是一个高效、友好的本地AI助手,名叫小O。你可以使用以下工具来帮助用户:
{tools}

请遵守以下规则:
1. 如果用户的问题可以直接回答,就不调用工具,直接回答。
2. 如果需要调用工具,严格按照工具的描述使用正确的参数。
3. 回答要简洁,不要啰嗦。
4. 你可以记住对话的上下文(通过记忆系统),根据上下文连贯回答。
"""),
    MessagesPlaceholder(variable_name="chat_history"),
    ("human", "{input}"),
    MessagesPlaceholder(variable_name="agent_scratchpad"),
])

注意:{tools} 会在 create_react_agent 中被自动填充,我们不需要手动替换。

命令行交互界面:像老朋友一样聊天

用 input() 循环是最简单的,但我们可以稍微美化一下:用 rich 库让输出更漂亮,或者直接用简单的 print。为了轻量,我们还是用 input,但加上一些提示符和颜色(用ANSI颜色码)。

def main():
    print("\033[1;36m🤖 本地AI助手 小O 已启动!\033[0m")
    print("💡 输入 'exit' 退出,输入 'clear' 清空记忆\n")
    while True:
        user_input = input("\033[1;32m你:\033[0m")
        if user_input.lower() in ['exit', 'quit']:
            print("\033[1;36m小O:再见!👋\033[0m")
            break
        elif user_input.lower() == 'clear':
            memory.clear()
            print("\033[1;33m小O:记忆已清空。\033[0m")
            continue
        
        response = agent_executor.invoke({"input": user_input})
        print(f"\033[1;36m小O:\033[0m{response['output']}\n")

优化运行效率:榨干每一毫秒

为了让Agent跑得更快,我们还可以做以下几点:

使用量化模型

  1. 前面已经说过,量化版本(如 qwen:7b-q4_0)比原始模型快30%~50%,内存占用也少。

    调整推理参数

  2. num_predict:限制输出长度。工具调用回答通常很短,256或512足够。

  3. temperature:0.1~0.3,避免模型发散。

  4. top_k:降低到10~20,加快采样。

    使用流式输出(可选)

  5. 如果希望用户看到逐字输出,可以在 AgentExecutor 中启用流式,但会增加代码复杂度。我们暂时不做,因为命令行下体验一般。

    缓存LLM调用

  6. 对于相同的输入,可以使用 langchain.cache 缓存结果,避免重复推理。但我们Agent是对话式的,相同输入较少,所以暂不实现。

    减少工具调用次数

  7. 优化提示词,让Agent尽量少调用工具。比如“现在几点了?”直接回答,而不需要调用时间工具(当然如果模型不知道时间,还是得调用)。

    异步调用(高级)

  8. 对于IO密集型工具(如网络请求),可以使用异步,但这里我们不深入。

完整代码:你的离线AI助手诞生了

下面给出今天所有代码的整合版本。你可以直接复制保存为 local_lightweight_agent.py

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
周六:本地轻量Agent开发
功能:离线运行的AI助手,支持多种工具、对话记忆、命令行交互
作者:小O
"""

import datetime
import random
import os
import subprocess
from langchain.tools import tool
from langchain_community.chat_models import ChatOllama
from langchain.memory import ConversationSummaryMemory
from langchain.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain.agents import create_react_agent, AgentExecutor

# ---------- 1. 定义工具 ----------
@tool
def get_weather(city: str) -> str:
    """查询指定城市的天气"""
    # 这里可以接入真实API,为演示简单模拟
    weather_db = {
        "北京": "晴,20-25℃",
        "上海": "多云,22-27℃",
        "广州": "阵雨,25-30℃",
        "深圳": "阴,24-28℃",
    }
    return f"{city}今天{weather_db.get(city, '天气未知')}。"

@tool
def calculator(expression: str) -> str:
    """计算数学表达式,例如 '2+2' 或 'sqrt(16)'"""
    try:
        import math
        result = eval(expression, {"__builtins__": {}}, math.__dict__)
        return f"计算结果:{result}"
    except Exception as e:
        return f"计算错误:{str(e)}"

@tool
def get_current_time(format: str = "%Y-%m-%d %H:%M:%S") -> str:
    """获取当前时间,可指定格式(例如 '%Y-%m-%d %H:%M:%S')"""
    return datetime.datetime.now().strftime(format)

@tool
def random_number(min_val: int = 0, max_val: int = 100) -> str:
    """生成一个随机整数,范围[min_val, max_val]"""
    return str(random.randint(min_val, max_val))

@tool
def write_note(filename: str, content: str) -> str:
    """将内容写入文件(如果文件不存在则创建),返回写入结果"""
    try:
        with open(filename, 'w', encoding='utf-8') as f:
            f.write(content)
        return f"笔记已保存到 {filename}"
    except Exception as e:
        return f"写入失败:{str(e)}"

@tool
def read_note(filename: str) -> str:
    """读取文件内容"""
    try:
        with open(filename, 'r', encoding='utf-8') as f:
            return f.read()
    except Exception as e:
        return f"读取失败:{str(e)}"

@tool
def run_command(command: str) -> str:
    """执行一条系统命令(例如 'ls -l'),返回输出结果(安全警告:请勿用于生产环境)"""
    try:
        result = subprocess.run(command, shell=True, capture_output=True, text=True, timeout=5)
        return result.stdout if result.stdout else result.stderr
    except Exception as e:
        return f"命令执行失败:{str(e)}"

# ---------- 2. 初始化本地模型(推荐量化版) ----------
llm = ChatOllama(
    model="qwen:7b-q4_0",   # 可换成 llama3:7b
    temperature=0.1,
    num_predict=256,
)

# ---------- 3. 设置记忆(摘要记忆,节省token) ----------
memory = ConversationSummaryMemory(
    llm=llm,
    memory_key="chat_history",
    return_messages=True,
    max_token_limit=2000,
)

# ---------- 4. 创建提示词模板 ----------
prompt = ChatPromptTemplate.from_messages([
    ("system", """你是一个高效、友好的本地AI助手,名叫小O。你可以使用以下工具来帮助用户:
{tools}

请遵守以下规则:
1. 如果用户的问题可以直接回答,就不调用工具,直接回答。
2. 如果需要调用工具,严格按照工具的描述使用正确的参数。
3. 回答要简洁,不要啰嗦。
4. 你可以记住对话的上下文(通过记忆系统),根据上下文连贯回答。
"""),
    MessagesPlaceholder(variable_name="chat_history"),
    ("human", "{input}"),
    MessagesPlaceholder(variable_name="agent_scratchpad"),
])

# ---------- 5. 创建Agent执行器 ----------
tools = [get_weather, calculator, get_current_time, random_number, write_note, read_note, run_command]
agent = create_react_agent(llm, tools, prompt)
agent_executor = AgentExecutor(
    agent=agent,
    tools=tools,
    memory=memory,
    verbose=False,                # 调试时可改为True
    handle_parsing_errors=True,
)

# ---------- 6. 命令行交互界面 ----------
def main():
    print("\033[1;36m🤖 本地AI助手 小O 已启动!\033[0m")
    print("💡 输入 'exit' 退出,输入 'clear' 清空记忆\n")
    while True:
        user_input = input("\033[1;32m你:\033[0m")
        if user_input.lower() in ['exit', 'quit']:
            print("\033[1;36m小O:再见!👋\033[0m")
            break
        elif user_input.lower() == 'clear':
            memory.clear()
            print("\033[1;33m小O:记忆已清空。\033[0m")
            continue
        
        response = agent_executor.invoke({"input": user_input})
        print(f"\033[1;36m小O:\033[0m{response['output']}\n")

if __name__ == "__main__":
    main()

运行演示:和你的AI宠物聊聊天

保存代码后,在终端运行:

python local_lightweight_agent.py

你会看到彩色的提示符,然后就可以和它对话了。试试下面这些:

你:现在几点了?
小O:现在是202532214:35:18。

你:帮我生成一个1100的随机数。
小O:76

你:写个笔记,文件名 test.txt,内容“今天学习了本地AI助手开发”。
小O:笔记已保存到 test.txt

你:帮我读一下 test.txt
小O:今天学习了本地AI助手开发

你:北京天气怎么样?
小O:北京今天晴,20-25℃。

你:那明天适合出门吗?
小O:根据天气预报,明天北京也是晴天,温度差不多,很适合出门哦。

你:clear
小O:记忆已清空。

你:exit
小O:再见!👋

注意:第二条“明天适合出门吗?”它没有调用天气工具,而是根据记忆中的天气信息回答了,说明记忆生效了。

总结

今天我们把过去一周的知识全部融合在一起,打造了一个功能强大、完全离线的本地轻量Agent。它:

  • ✅ 基于LangChain + Ollama,离线运行

  • ✅ 拥有丰富的工具(天气、计算器、时间、文件操作、随机数等)

  • ✅ 具备记忆(摘要记忆,节省token)

  • ✅ 有友好的命令行交互界面

  • ✅ 经过参数优化,响应速度更快

常见问题解答

Q:为什么我的Agent总是调用工具,哪怕可以直接回答?
A:可以降低temperature,或者在系统提示中强调“如果可以直接回答就不要调用工具”。另外,工具描述要清晰,避免误导。

Q:使用ConversationSummaryMemory时,为什么有时会慢?
A:因为每次对话后,它需要调用LLM生成摘要。如果对话轮数很多,摘要会累积。可以适当调整max_token_limit,或换用ConversationBufferWindowMemory

Q:执行 **run_command** 工具时,提示安全警告怎么办?
A:确实有安全风险,建议在生产环境中移除该工具,或者对命令进行白名单过滤。这里仅用于学习演示。

Q:本地模型跑起来很卡,有什么绝招?
A:除了量化,还可以尝试更小的模型(如 qwen:4b),或者减少num_predict。另外,确保没有其他占用GPU的程序。

Q:我想添加更多工具,比如发送邮件、查询股票,怎么做?
A:只需定义新的@tool函数,加入tools列表即可。注意函数的docstring要准确,参数类型清晰。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐