1. 项目概述与核心价值
最近在折腾本地大模型应用的时候,发现了一个挺有意思的项目,叫Awareness-Local。这个项目名直译过来是“本地意识”,听起来有点玄乎,但它的核心目标非常明确:让大型语言模型(LLM)在完全离线的环境下,也能拥有对当前时间、日期、用户身份以及本地文件系统的“感知”能力。
简单来说,它解决了一个大模型本地化部署中的“失忆”问题。我们平时用的ChatGPT或者云端API,服务器是知道当前时间的,也能根据你的历史对话进行一定程度的上下文理解。但当你把模型(比如Llama、Qwen、ChatGLM)下载到自己的电脑上,通过Ollama、LM Studio或者text-generation-webui来运行时,这个模型就像一个与世隔绝的“天才”:它知识渊博,却不知道“今夕是何年”,也不知道你是谁,更无法主动读取你电脑里的文档来回答问题。
Awareness-Local项目就是为了填补这个空白。它不是一个独立的AI应用,而是一个智能体(Agent)框架的增强插件或系统提示词工程的最佳实践集合。通过精心设计的系统提示词(System Prompt)和一套可选的、轻量级的本地工具调用方案,它能让你的本地大模型“睁开眼”,获得基础的上下文感知能力。这对于构建真正实用、个性化的本地AI助手至关重要,比如让它帮你整理今天的日程、总结你刚写的文档、或者基于你本地知识库进行问答。
这个项目的价值在于其“轻量”与“即插即用”。它不要求你重新训练模型,也不依赖复杂的云端服务,而是利用了大模型本身遵循指令和上下文学习的能力,通过“告诉”它该知道什么信息,来显著提升对话的实用性和准确性。接下来,我们就深入拆解它的设计思路、具体实现以及如何将它融入到你现有的本地AI工作流中。
2. 核心设计思路与架构拆解
Awareness-Local的设计哲学非常清晰:在最小化侵入性和复杂度的前提下,最大化上下文信息的有效性。它主要从两个层面来实现“感知”:
2.1 信息注入层:动态系统提示词
这是项目的核心。传统的本地大模型对话,系统提示词往往是静态的,例如“你是一个有帮助的AI助手”。Awareness-Local将其动态化。在每次对话或会话开始时,它会自动生成一个包含实时信息的增强型系统提示词。
这个动态提示词通常包含以下几个关键模块:
- 时间与日期:通过调用系统API获取当前的精确时间、日期、星期几,甚至季节。这解决了模型“不知道时间”的根本问题。
- 用户身份:可以注入一个预设的用户名(如“开发者Edwin”、“用户小明”),让模型的回复更具针对性。更高级的实现可以关联系统登录用户名。
- 会话上下文:声明本次对话的上下文范围或目标,例如“本次对话将围绕用户本地项目文档展开”。
- 能力与约束声明:明确告诉模型它现在具备“感知”能力,并规定这些信息的使用方式,例如“你知道当前时间,但仅在回答相关问题时主动提及,避免在每个回复开头都重复时间信息”。
这种设计的巧妙之处在于,它完全遵循了大模型的运作原理。系统提示词是模型在生成回复前最先“看到”并深度理解的文本。将关键上下文信息放在这里,相当于在模型思考的“底层操作系统”里写入了环境变量,效果比在用户提问中附带这些信息要稳定和显著得多。
2.2 工具扩展层:轻量级本地函数调用
对于一些更复杂的感知需求,比如“读取我桌面上的report.md文件并总结”,仅靠提示词是不够的。这就需要模型具备“动手能力”。
Awareness-Local的另一个设计重点是集成了一套本地化的工具调用(Function Calling)框架。这里的“工具”指的是运行在你电脑本地的、有严格权限控制的小程序或函数。例如:
- 文件系统工具:安全地列出目录、读取指定文件内容(需用户显式授权路径)。
- 系统信息工具:获取更详细的系统状态,如CPU/内存使用率、网络状态(如果项目涉及)。
- 应用程序交互工具(进阶):发送指令到其他本地应用,例如让音乐播放器暂停。
这个工具层通常通过一个轻量级的“工具服务器”或“适配层”来实现。当模型根据对话判断需要调用工具时(例如用户说“看看我的笔记”),它会输出一个结构化的请求。这个请求被本地代理捕获,然后执行对应的安全本地函数,将结果(如文件内容)作为新的上下文送回给模型,由模型生成最终回复。
注意:工具调用是双刃剑。它极大地增强了能力,但也带来了复杂性和安全风险。
Awareness-Local的设计通常会强调“最小权限原则”和“显式授权”,例如工具只能访问预先配置的少数几个“沙盒”目录,而不是整个硬盘。
2.3 架构总览
整个项目的架构可以理解为一种“夹心层”模式:
用户 <-> [前端/聊天界面] <-> [Awareness-Local 代理层] <-> [本地大模型] | [动态提示词引擎] + [本地工具集] | [系统时钟/文件API]- 代理层:作为中间件,拦截所有发送给模型的请求。它的工作是:1) 向原始用户消息前附加动态生成的系统提示词;2) 解析模型的输出,看是否包含工具调用请求;3) 执行工具并组织新一轮的对话上下文。
- 动态提示词引擎:一个简单的脚本或模块,负责收集时间、用户信息等,并按照模板生成最终的提示词。
- 本地工具集:一系列安全的Python函数或其他可执行模块,用于执行具体的本地操作。
这种架构确保了它能够灵活地适配各种已有的本地大模型部署方案(Ollama, OpenWebUI, llama.cpp等),你只需要稍微修改它们的调用方式,指向这个“代理层”即可。
3. 关键组件与实现细节解析
理解了整体思路,我们来看看要把Awareness-Local跑起来,具体需要哪些组件,以及如何配置它们。这里我们以一个典型的基于Python的实现为例。
3.1 动态提示词模板
这是项目的灵魂。一个好的模板既要信息全面,又要避免冗余,防止占用过多宝贵的上下文窗口(Context Window)。下面是一个高度可配置的模板示例:
# awareness_prompt_template.py import datetime import getpass class AwarenessPromptGenerator: def __init__(self, user_name=None, allowed_dirs=None): self.user_name = user_name or getpass.getuser() # 默认使用系统用户名 self.allowed_dirs = allowed_dirs or [] # 允许访问的目录列表 def generate_system_prompt(self): # 获取当前时间信息 now = datetime.datetime.now() current_date = now.strftime("%Y年%m月%d日") current_time = now.strftime("%H:%M:%S") day_of_week = ["星期一", "星期二", "星期三", "星期四", "星期五", "星期六", "星期日"][now.weekday()] # 构建核心提示词 prompt = f"""你是一个运行在用户本地计算机上的智能助手。你被赋予了以下上下文感知能力: 1. **时间感知**:你知道当前的日期和时间。 - 当前日期:{current_date} ({day_of_week}) - 当前时间:{current_time} 2. **用户感知**:你知道正在与你对话的用户身份。 - 当前用户:{self.user_name} 3. **文件系统感知**:在用户明确授权和请求下,你可以协助处理本地文件。请注意,你只能访问以下明确允许的目录路径: {chr(10).join([' - ' + path for path in self.allowed_dirs]) if self.allowed_dirs else ' - (暂无配置的授权目录)'} - 任何文件操作都必须基于用户的直接指令,且不能超出上述范围。 **重要指令**: - 请自然地运用这些信息来使你的回答更准确、更有帮助。例如,当用户询问“今天是什么日子?”时,直接回答日期和星期。 - 除非用户询问或上下文需要,避免在每条回复的开头都重复时间和用户信息。 - 当用户请求涉及文件操作时,请明确说明你将进行的操作(例如,“我将读取你‘文档’文件夹下的‘报告.md’文件”),并在操作前等待用户最终确认(如果实现确认机制)。 - 你的核心目标是提供安全、有用、精准的本地化协助。 现在,对话开始。""" return prompt关键点解析:
- 模块化信息:将时间、用户、文件权限分点说明,结构清晰,便于模型理解。
- 安全强调:反复强调文件访问的限制和需要“明确授权”,这是本地AI安全性的基石。
- 使用指导:告诉模型“如何”使用这些信息(自然运用,避免冗余),这能显著提升回复质量,避免模型变成复读机。
3.2 本地工具调用实现
工具调用需要模型、代理、执行端三方的协议。一种简单实现是使用类似 OpenAI 的 Function Calling 格式。
首先,定义工具列表并告知模型:
# local_tools.py import json import os from pathlib import Path from typing import List, Optional def list_directory(path: str) -> str: """列出指定目录下的文件和文件夹。""" try: base_path = Path(path).resolve() # 这里应加入安全检查,确保path在allowed_dirs内 items = os.listdir(base_path) return f"目录 '{path}' 下的内容:\n" + "\n".join(items) except Exception as e: return f"列出目录时出错:{e}" def read_file_content(file_path: str) -> str: """读取指定文件的内容。""" try: # 安全检查 with open(file_path, 'r', encoding='utf-8') as f: content = f.read(5000) # 限制读取长度,防止上下文爆炸 return f"文件 '{file_path}' 的内容(前5000字符):\n```\n{content}\n```" except Exception as e: return f"读取文件时出错:{e}" # 定义工具的描述,用于生成给模型看的“工具说明书” TOOLS = [ { "type": "function", "function": { "name": "list_directory", "description": "列出指定目录下的文件和子目录列表。用于浏览用户允许访问的文件夹。", "parameters": { "type": "object", "properties": { "path": {"type": "string", "description": "要列出的目录的绝对路径或相对于授权根目录的路径。"} }, "required": ["path"] } } }, { "type": "function", "function": { "name": "read_file_content", "description": "读取指定文本文件的内容。用于查看文档、日志或代码文件。", "parameters": { "type": "object", "properties": { "file_path": {"type": "string", "description": "要读取的文件的绝对路径或相对于授权根目录的路径。"} }, "required": ["file_path"] } } } ]然后,在代理层中,需要处理模型可能返回的工具调用请求。以下是代理逻辑的核心片段:
# awareness_agent.py (部分代码) import requests import json class AwarenessAgent: def __init__(self, llm_api_url, prompt_generator): self.llm_api_url = llm_api_url # 例如 Ollama 的 API 地址 self.prompt_gen = prompt_generator def chat_round(self, user_message, conversation_history=[]): # 1. 生成本次对话的动态系统提示词 system_prompt = self.prompt_gen.generate_system_prompt() # 2. 构建完整的消息历史,将系统提示词放在最前面 messages = [{"role": "system", "content": system_prompt}] messages.extend(conversation_history) messages.append({"role": "user", "content": user_message}) # 3. 准备请求体,将工具定义也发送给模型 request_body = { "model": "qwen2.5:7b", # 你实际使用的模型 "messages": messages, "tools": TOOLS, # 传入工具定义 "tool_choice": "auto", # 让模型自行决定是否调用工具 "stream": False } # 4. 发送请求到本地LLM服务 response = requests.post(self.llm_api_url, json=request_body) llm_response = response.json() # 5. 解析响应,检查是否有工具调用 message = llm_response.get('message', {}) tool_calls = message.get('tool_calls', None) final_reply = "" if tool_calls: # 6. 执行工具调用 tool_results = [] for tool_call in tool_calls: func_name = tool_call['function']['name'] args = json.loads(tool_call['function']['arguments']) if func_name == "list_directory": result = list_directory(args['path']) elif func_name == "read_file_content": result = read_file_content(args['file_path']) else: result = f"未知工具:{func_name}" tool_results.append({ "tool_call_id": tool_call['id'], "role": "tool", "name": func_name, "content": result }) # 7. 将工具执行结果作为新消息,再次发送给模型,让它生成面向用户的最终回复 messages.append(message) # 加入模型刚才的回复(包含工具调用请求) messages.extend(tool_results) # 加入工具执行结果 # 发起第二轮请求,让模型“消化”工具结果并生成最终回答 second_request_body = {**request_body, "messages": messages} second_response = requests.post(self.llm_api_url, json=second_request_body) final_message = second_response.json().get('message', {}) final_reply = final_message.get('content', '') else: # 没有工具调用,直接返回模型回复 final_reply = message.get('content', '') return final_reply实操心得:
- 安全检查是生命线:在
list_directory和read_file_content函数中,必须添加路径验证逻辑,确保请求的路径在allowed_dirs列表内或其子目录下。绝对不能让模型拥有自由访问整个文件系统的能力。 - 上下文管理:工具调用涉及多轮消息交换。要妥善管理
messages列表,确保系统提示词、历史对话、用户问题、工具调用请求和工具结果都在正确的位置,否则模型会感到“困惑”。 - 性能考量:文件读取最好设置长度限制(如上面的5000字符),因为大模型上下文窗口有限,过长的文件内容会挤占对话空间,也可能增加API调用成本(token数)。
4. 集成与部署实战指南
理论讲完了,我们来点实际的。如何将Awareness-Local的能力集成到你现有的本地AI环境中?这里以最流行的Ollama + Open WebUI组合为例。
4.1 基础环境准备
假设你已经安装了 Ollama 并拉取了模型(如ollama pull qwen2.5:7b),同时也部署了 Open WebUI。我们的目标是在 Open WebUI 中,让模型获得感知能力。
方案一:修改 Open WebUI 的系统提示词模板(简易版)这是最快的方法,但只能实现“时间/用户感知”,无法实现工具调用。
- 登录 Open WebUI 管理界面。
- 进入 “模型” -> 选择你使用的模型(如
qwen2.5:7b)。 - 找到 “系统提示词” 或 “Model Settings” 区域。
- 将我们之前
AwarenessPromptGenerator生成的动态提示词(需要你写个小脚本提前生成好)粘贴进去。但问题是,这个提示词是静态的,时间不会自动更新。 - 变通方案:你可以写一个简单的脚本,定时(比如每小时)调用
AwarenessPromptGenerator生成新提示词,并通过 Open WebUI 的 API 更新模型设置。这比较 hacky,但可行。
方案二:部署 Awareness 代理服务器(推荐,功能完整)这是更优雅和强大的方式。我们创建一个独立的 FastAPI 应用作为代理,它位于 Open WebUI 和 Ollama 之间。
项目结构:
awareness_local_proxy/ ├── main.py # FastAPI 主应用 ├── prompt_gen.py # 动态提示词生成器 ├── local_tools.py # 工具函数定义 ├── config.yaml # 配置文件(模型地址、授权目录等) └── requirements.txtmain.py核心代码:from fastapi import FastAPI, HTTPException from fastapi.responses import StreamingResponse import requests import json from prompt_gen import AwarenessPromptGenerator import local_tools import asyncio app = FastAPI(title="Awareness Local Proxy") PROMPT_GEN = AwarenessPromptGenerator(user_name="LocalUser", allowed_dirs=["/Users/YourName/Documents", "/Users/YourName/Desktop"]) OLLAMA_URL = "http://localhost:11434/api/chat" # Ollama 默认API地址 @app.post("/v1/chat/completions") async def chat_completion(request: dict): # 1. 提取用户消息和历史 messages = request.get("messages", []) model = request.get("model", "qwen2.5:7b") # 提取最后一条用户消息 user_msg = None for msg in reversed(messages): if msg["role"] == "user": user_msg = msg["content"] break if not user_msg: raise HTTPException(status_code=400, detail="No user message found") # 2. 生成动态系统提示词 system_prompt = PROMPT_GEN.generate_system_prompt() # 3. 重组消息,将动态系统提示词插入最前 # 注意:移除原有的静态system消息(如果有) new_messages = [{"role": "system", "content": system_prompt}] for msg in messages: if msg["role"] != "system": # 过滤掉旧的静态system消息 new_messages.append(msg) # 4. 构建转发给Ollama的请求 ollama_payload = { "model": model, "messages": new_messages, "stream": request.get("stream", False), "options": request.get("options", {}), "tools": local_tools.TOOLS # 传入工具定义 } # 5. 处理流式和非流式响应 if ollama_payload["stream"]: async def generate(): # 这里简化处理,实际需处理流式响应中的工具调用,逻辑更复杂 async with aiohttp.ClientSession() as session: async with session.post(OLLAMA_URL, json=ollama_payload) as resp: async for chunk in resp.content: yield chunk return StreamingResponse(generate(), media_type="application/x-ndjson") else: # 非流式处理(简化版,完整工具调用逻辑参考第3.2节) response = requests.post(OLLAMA_URL, json=ollama_payload) return response.json() if __name__ == "__main__": import uvicorn uvicorn.run(app, host="0.0.0.0", port=8000)配置 Open WebUI:
- 启动你的代理服务器:
python main.py。 - 打开 Open WebUI 设置,找到 “连接设置” 或 “API 配置”。
- 将 “Ollama API Base URL” 从
http://localhost:11434修改为http://localhost:8000(你的代理服务器地址)。 - 保存。现在,所有通过 Open WebUI 发起的对话,都会先经过你的
Awareness Local Proxy,由它注入动态提示词并处理潜在的工具调用,再转发给 Ollama。
- 启动你的代理服务器:
部署注意事项:
- 路径安全:在
config.yaml或环境变量中配置allowed_dirs,务必使用绝对路径,并限制在最小必要范围。 - 错误处理:代理服务器必须有完善的错误处理,当 Ollama 服务宕机或工具调用出错时,给前端返回友好的错误信息。
- 性能:代理会增加少量延迟。确保你的代理服务器和 Ollama 运行在同一台机器或局域网内,以减少网络开销。
4.2 与更多客户端集成
除了 Open WebUI,你的代理服务器遵循了部分 OpenAI API 格式,这意味着任何兼容 OpenAI API 的客户端(如ChatGPT-Next-Web,LangChain,LlamaIndex)理论上都可以通过修改 API Base URL 来连接你的代理,从而获得感知能力。
例如,在ChatGPT-Next-Web的部署中,你只需在环境变量OPENAI_API_BASE里填入http://your-server:8000/v1,它就会将请求发送到你的代理。
5. 高级技巧与优化策略
基础功能跑通后,我们可以追求更丝滑、更智能的体验。以下是一些进阶思路:
5.1 上下文记忆的持久化
目前的动态提示词只在单次请求中有效。要实现跨会话的记忆(比如记住用户偏好),需要引入外部记忆机制。
- 向量数据库记忆:使用
ChromaDB或LanceDB等轻量级向量库。将每轮对话的核心摘要(由模型生成)向量化后存储。每次生成系统提示词时,先查询向量库,找到与当前对话最相关的历史记忆片段,一并注入提示词。这能让模型拥有“长期记忆”。 - 摘要式记忆:一种更简单的方法是在每次对话结束时,让模型自己总结本次对话的要点(例如:“用户询问了关于Python装饰器的问题,我给出了解释和示例。”),然后将这个摘要以文本形式追加到一个日志文件中。下次对话时,将这个文件的内容作为上下文的一部分读入。
5.2 工具调用的优化与安全增强
- 工具描述优化:
local_tools.py中工具函数的description字段至关重要。描述越清晰、具体,模型调用工具的准确率越高。可以加入示例,如“description”: “列出目录内容。例如,当用户说‘看看我的文档文件夹里有什么’时使用此工具。参数path应为类似‘/Users/xxx/Documents’的绝对路径。” - 用户确认机制:对于高风险操作(如文件删除、执行命令),不要直接执行。可以让模型在请求工具时,生成一段需要用户确认的文本,例如:“我准备删除‘test.txt’文件,此操作不可逆。请确认是否继续?(是/否)”。代理层捕获到这个特殊回复后,先将其返回给用户,等待用户明确输入“是”之后,再真正执行工具调用。
- 工具结果摘要:当工具返回的结果非常长(如一个大文件的内容),直接塞回上下文可能效率低下。可以让模型先对结果进行摘要,例如:“该文件内容主要讨论了……核心观点有三:1…2…3…”。然后将摘要而非全文作为工具执行结果返回。
5.3 提示词工程的微调
动态提示词模板不是一成不变的。你需要根据所用模型的特性进行微调。
- 指令跟随能力测试:有些较小的模型指令跟随能力较弱。你可能需要更强硬、更重复的指令,比如用“你必须”、“你务必”等词语,并将最重要的指令放在提示词的开头和结尾。
- 减少幻觉:对于文件内容,可以在提示词中强调:“你关于文件内容的回答,必须严格基于工具读取到的实际文本,不得捏造或推测文件中不存在的信息。”
- 个性化:根据你的主要用途调整提示词权重。如果你主要用于编程,可以加强“代码理解与生成”相关指令;如果用于写作,则可以强调“风格模仿”和“灵感激发”。
6. 常见问题与故障排除
在实际搭建和使用过程中,你肯定会遇到各种问题。这里记录一些典型坑位和解决方案。
6.1 模型不调用工具或调用错误
- 症状:用户明明说“读一下我的笔记”,模型却自顾自地开始编造笔记内容,而不触发
read_file_content工具。 - 排查步骤:
- 检查工具描述:工具的描述是否足够清晰?模型是否理解在什么场景下该调用这个工具?尝试用更直白的语言重写描述。
- 检查消息结构:确保发送给模型的
messages列表中包含了tools字段,并且格式正确。使用print或日志功能输出你最终发给 Ollama 的完整请求体,对比官方文档。 - 检查模型能力:并非所有模型都很好地支持工具调用。确保你使用的模型版本是较新的,并且官方文档声明支持
function calling或tool use。Qwen2.5、DeepSeek最新版、Llama 3.1等在这方面表现较好。 - 简化测试:先从一个最简单的工具(如“获取当前时间”)开始测试,确保整个调用链路是通的。
6.2 动态提示词导致回复质量下降
- 症状:注入动态提示词后,模型变得啰嗦、总是重复时间信息,或者反而更“笨”了。
- 解决方案:
- 精简提示词:过长的系统提示词会占用本应用于理解用户问题的上下文窗口。尽量压缩信息,只保留最必要的。例如,时间信息可以精简为一行。
- 调整指令位置:将最重要的行为指令(如“避免重复时间信息”)放在系统提示词的最末尾。研究表明,模型对提示词开头和结尾的内容记忆更深刻。
- 使用模型原生能力:一些新的模型(如
Llama 3)在创建时就可以设置“系统提示词”。如果 Ollama 或你的 WebUI 支持直接设置,可以优先使用这种方式,可能比通过消息列表插入更稳定。
6.3 代理服务器性能瓶颈
- 症状:对话响应明显变慢,尤其是进行文件操作时。
- 优化方向:
- 缓存:对于不常变化的信息(如用户名、授权的目录列表),可以缓存在内存中,不必每次请求都重新生成。
- 异步处理:使用
async/await(如httpx.AsyncClient)来处理所有网络 I/O(调用 Ollama API、读取大文件),避免阻塞。 - 流式响应透传:对于不涉及工具调用的纯文本对话,代理服务器应该直接将 Ollama 的流式响应透传给前端,而不是等待全部完成再返回。这能极大提升首字响应时间。
- 工具超时:为文件读取等工具设置超时时间,防止因读取一个巨大文件而卡死整个请求。
6.4 安全疑虑与权限控制
这是最重要的问题。永远不要信任来自模型的任意路径。
- 症状:模型请求读取
/etc/passwd或C:\Windows\System32\config\SAM。 - 防护措施:
- 路径白名单:这是底线。在工具函数内部,必须将请求的路径解析为绝对路径,并与预先配置的白名单列表进行比对。不在列表内的路径,直接返回“无权访问”。
- 路径规范化:防止目录遍历攻击。使用
os.path.normpath和os.path.abspath规范化路径,并检查规范化后的路径是否以白名单目录开头。 - 沙盒环境:考虑在 Docker 容器或虚拟机中运行整个代理和模型,即使出现安全问题,也能被隔离。
- 审计日志:记录所有工具调用的时间、用户、参数和结果。定期审查日志,发现异常行为。
最后,Awareness-Local这类项目的魅力在于它用相对简单的工程手段,释放了本地大模型的巨大潜力。它不是一个开箱即用的完美产品,而是一个需要你根据自身需求去调整、打磨的框架。从注入第一行动态时间开始,到安全地让模型帮你整理文档,每一步的实践都会让你对智能体、提示词工程以及本地AI应用有更深的理解。