1. 从“只会聊天”到“真能干活”:我为什么要做 OpenWorkMate
公司里那套 AI 助手,我用了快一年,说实话,前半年新鲜,后半年烦。你问它“上周三的会议纪要里提到的那个供应商报价是多少”,它要么装傻,要么给你编一个看起来很像真的数字。你让它“把这份合同里的付款节点整理成表格”,它倒是能生成,但生成完你还得自己复制粘贴到 Excel 里,再手动核对一遍,生怕它把“30 天”写成“3 天”。这就是典型的“只会聊天”——它能陪你唠,但真让它干活,你得在旁边盯着,比自己干还累。
我一直在想,能不能让 AI 真正变成“工作伙伴”,而不是“聊天玩具”。不是那种你问一句它答一句的被动模式,而是它能主动理解你的工作上下文,能调用你本地的文件、数据库、内部系统,能记住你上周交代过的事情,能在你还没开口的时候就提醒你“那个供应商的合同今天该催了”。这个想法在我脑子里转了大概三个月,直到 GPT‑6 的 API 开放了更细粒度的工具调用和长期记忆接口,我才觉得时机到了。
OpenWorkMate 这个项目,就是在这个背景下动手的。它的核心定位很简单:一个可以私有部署的、能接入企业内部数据和工具的 AI 工作伙伴。不是 SaaS,不是那种你把公司机密上传到别人服务器上的东西。它跑在你自己的机器上,用 Ollama 或者兼容 OpenAI 接口的本地模型做推理,用向量数据库做长期记忆,用插件系统对接你的邮件、日历、文档、代码仓库。你可以把它理解成一个“有手有脚”的 AI,它不光能说,还能做。
适合谁来参考?如果你是一个中小团队的技术负责人,想给团队搞一个内部 AI 助手但又不放心数据外流;如果你是一个独立开发者,想研究 AI Agent 的工程化落地;或者你就是一个对“AI 到底怎么才能真干活”这件事好奇的从业者,那这篇内容应该能给你一些可以直接抄作业的东西。我会把整个架构设计、核心模块的实现思路、踩过的坑、以及那些文档里不会写的经验,都摊开来讲。
2. 整体架构设计:为什么我不建议你上来就堆功能
2.1 核心思路:把“记忆”和“行动”拆开做
很多人在做 AI 工作伙伴的时候,第一反应是“我要让它什么都能干”。于是上来就接十几个 API,写几十个工具函数,结果发现模型根本不知道该在什么时候调用哪个工具,或者调用了但参数传错了,整个系统变得极其脆弱。我一开始也差点掉进这个坑,后来强迫自己把架构拆成三层:感知层、记忆层、行动层。
感知层负责理解用户输入和上下文,包括当前对话、选中的文档、剪贴板内容、甚至当前打开的窗口标题。记忆层负责存储和检索长期信息,包括用户偏好、历史任务、实体关系。行动层负责实际执行,包括调用工具、生成文件、发送请求。这三层之间通过一个“编排器”来协调,编排器的核心逻辑是:先查记忆,再决定要不要行动,行动之后把结果写回记忆。
为什么这么拆?因为如果你把记忆和行动混在一起,模型很容易在“该记住的时候去行动”或者“该行动的时候在瞎回忆”。拆开之后,每一层的职责清晰,调试也方便。比如你发现它老是忘记你上周说的“报价单要用蓝色模板”,那你就去查记忆层的写入逻辑;如果它老是调用错误的工具,那你就去查行动层的工具描述和参数校验。
2.2 模型选型:GPT‑6 不是唯一解,但它是目前最省心的
标题里提到了 GPT‑6,我确实用了 GPT‑6 的 API 来做核心推理,但我想说清楚:OpenWorkMate 并不绑定 GPT‑6。它的模型接口是抽象的,你可以换成任何兼容 OpenAI 接口的本地模型,比如通过 Ollama 部署的 Llama 系列、Qwen 系列,或者公司内部微调的模型。我用 GPT‑6 的原因很简单:它的工具调用(Function Calling)稳定性和长期记忆的上下文窗口在目前我试过的模型里是最好的。
具体来说,GPT‑6 在以下几个方面比之前的版本有明显提升:第一,工具调用的参数准确率高了,以前你定义一个“发送邮件”的工具,它经常把收件人和抄送人搞混,现在基本不会;第二,长上下文里的信息检索更准了,你给它 50 页的会议记录,它能准确找到三个月前某个人说的一句话;第三,它对“什么时候不该调用工具”的判断更合理了,不会你随便问个天气它就跑去查数据库。
但如果你预算有限或者数据绝对不能出内网,我建议你用 Ollama 部署一个 70B 级别的模型,比如 Qwen2.5-72B 或者 Llama3.3-70B,效果虽然比 GPT‑6 差一点,但配合好的提示词工程和工具设计,也能达到可用的程度。我实测下来,在“整理文档”和“查询内部知识库”这两个场景下,本地模型的准确率大概能到 GPT‑6 的 85% 左右,但成本是零。
2.3 私有部署的底线:数据不出内网
这个项目从第一天起,我就定了一个死规矩:任何用户数据都不能离开部署环境。这意味着几件事:第一,模型推理要么用本地模型,要么用企业自己申请的 API 密钥,但请求必须走企业自己的网关,不能直接暴露给第三方;第二,向量数据库和记忆存储必须本地化,我用的是 ChromaDB 和 SQLite 的组合,轻量且够用;第三,所有工具调用的日志必须脱敏后存储,不能把原始内容直接写进日志文件。
为什么这么强调私有部署?因为我见过太多团队为了图方便,直接把内部文档扔给某个在线 AI 服务,结果不小心把客户名单或者财务数据传上去了。OpenWorkMate 的设计原则是:默认安全,显式授权。任何工具在调用之前,都必须经过一个权限检查模块,确认当前用户有权限访问该工具对应的数据源。这个检查不是靠模型判断,而是靠代码硬编码的规则。
2.4 插件系统:让 AI 的“手”可以无限延伸
OpenWorkMate 的插件系统是我最花心思的部分。每个插件就是一个独立的 Python 包,里面定义了工具函数、参数 schema、权限要求和执行逻辑。编排器在启动时会扫描插件目录,把所有工具注册到一个中央注册表里。当模型决定调用某个工具时,编排器会根据工具名找到对应的插件,检查权限,然后执行。
插件的设计有几个关键点:第一,工具描述必须极其清晰,包括它做什么、什么时候用、参数格式是什么、返回值是什么。我试过把工具描述写得模棱两可,结果模型要么不用,要么乱用。第二,参数校验必须严格,模型传过来的参数不能直接信任,必须经过 Pydantic 或者类似的校验层。第三,执行结果必须结构化,不能返回一大段自然语言,否则模型很难从中提取有用信息。
目前我内置了几个核心插件:文件操作(读写本地文档)、邮件(通过 IMAP/SMTP)、日历(通过 CalDAV)、代码仓库(通过 Git)、知识库检索(通过向量数据库)。每个插件都可以单独启用或禁用,也可以根据用户角色设置不同的权限。
3. 核心模块拆解:记忆、工具调用与编排器
3.1 长期记忆模块:怎么让 AI 记住“上周三说的那件事”
长期记忆是 OpenWorkMate 区别于普通聊天机器人的核心。我把它分成三种类型:事实记忆、偏好记忆、任务记忆。事实记忆存储的是客观信息,比如“供应商 A 的报价是 12 万”、“项目 X 的截止日期是 3 月 15 日”。偏好记忆存储的是用户习惯,比如“报价单用蓝色模板”、“邮件开头要加‘您好’”。任务记忆存储的是进行中的任务状态,比如“合同审核:已收到法务反馈,待修改”。
存储结构上,我用 SQLite 存结构化的事实和任务,用 ChromaDB 存非结构化的对话片段和文档摘要。每次用户输入时,编排器会先从 SQLite 里查相关的事实和任务,再从 ChromaDB 里检索语义相似的对话片段,然后把这两部分拼接到提示词里。这里有个关键技巧:不要把所有记忆都塞进上下文,那样会稀释模型的注意力。我的做法是先用一个轻量级的检索模型(比如 bge-small)做粗筛,再用 GPT‑6 做精排,只把最相关的 5 到 10 条记忆放进提示词。
记忆的写入时机也很重要。我试过每轮对话都写入,结果数据库膨胀得很快,而且很多噪音。后来改成事件驱动写入:只有当模型明确表示“这个信息值得记住”或者用户说“记住这个”时,才触发写入。具体实现是在工具集里加一个save_memory工具,模型可以主动调用它。同时,编排器会在每轮对话结束后,用一个轻量级的分类器判断是否需要自动保存。
3.2 工具调用:从“能调”到“调得对”
工具调用的准确性是决定 AI 工作伙伴好不好用的关键。我踩过的坑包括:模型把“查询”当成“修改”、参数类型传错、调用不存在的工具、以及最头疼的——该调用工具的时候不调用,不该调用的时候瞎调用。
解决这些问题,我总结了几个实操要点。第一,工具命名要语义化且唯一,不要用do_stuff这种名字,要用query_supplier_quote这种一看就知道干什么的名字。第二,参数描述要包含示例,比如date参数要写“格式:YYYY-MM-DD,例如 2025-03-15”。第三,在系统提示词里明确工具调用的边界,比如“当用户询问内部数据时,必须先调用检索工具,不得直接回答”。第四,加一个“确认”机制,对于修改类操作(比如发送邮件、删除文件),编排器会先返回一个确认请求,等用户确认后再执行。
我还做了一个“工具调用日志”模块,每次调用都记录:调用了哪个工具、参数是什么、返回结果是什么、耗时多少、是否成功。这个日志在调试的时候极其有用,你可以清楚地看到模型在每一步的决策依据。
3.3 编排器:整个系统的大脑
编排器是 OpenWorkMate 的核心逻辑所在,它负责协调感知、记忆、行动三层。它的工作流程大致是这样的:接收用户输入 → 检索相关记忆 → 构建提示词 → 调用模型 → 解析模型输出 → 如果需要调用工具,执行工具 → 把工具结果返回给模型 → 生成最终回复 → 写入记忆。
这个流程看起来简单,但每个环节都有坑。比如“构建提示词”这一步,我试过把记忆、工具描述、系统指令、对话历史全部拼在一起,结果提示词长度经常超过模型的上下文窗口。后来我做了动态裁剪:根据当前任务类型,只保留最相关的记忆和工具描述。比如用户问的是“合同”相关的问题,那就只保留合同相关的工具和记忆,其他的一律不放进提示词。
还有一个关键设计是超时和重试。工具调用可能会失败,模型可能会返回无法解析的输出,这些都需要编排器来处理。我的做法是:工具调用设置 30 秒超时,失败后重试一次;模型输出解析失败时,把错误信息返回给模型,让它重新生成,最多重试两次。如果还是失败,就返回一个友好的错误提示,而不是让整个系统崩溃。
4. 实操过程:从零部署一个 OpenWorkMate
4.1 环境准备与依赖安装
先说硬件要求。如果你用 GPT‑6 的 API,那本地机器只需要能跑 Python 和数据库就行,一台 8GB 内存的笔记本足够。如果你要用本地模型,那建议至少 32GB 内存,最好有 24GB 显存的 GPU,这样能跑 70B 级别的量化模型。我用的是 RTX 4090 + 64GB 内存的配置,跑 Qwen2.5-72B 的 4-bit 量化版本,推理速度大概每秒 15 到 20 个 token,日常使用够了。
软件依赖方面,核心是 Python 3.11 以上、Ollama(如果用本地模型)、ChromaDB、SQLite、以及一些常用的库比如fastapi、pydantic、httpx。我建议用uv来管理依赖,比 pip 快很多。安装命令大概是这样的:
# 安装 uv curl -LsSf https://astral.sh/uv/install.sh | sh # 创建虚拟环境 uv venv openworkmate-env source openworkmate-env/bin/activate # 安装核心依赖 uv pip install fastapi uvicorn pydantic httpx chromadb sqlite-utils python-dotenv如果你要用 Ollama 跑本地模型,还需要单独安装 Ollama 并拉取模型:
# 安装 Ollama(具体命令参考官方文档) # 拉取模型 ollama pull qwen2.5:72b-instruct-q4_K_M注意:Ollama 的模型拉取需要足够的磁盘空间,70B 的 4-bit 量化模型大概需要 40GB 左右。建议提前清理磁盘。
4.2 配置文件与核心参数设置
OpenWorkMate 的配置文件我设计成 YAML 格式,放在项目根目录的config.yaml里。核心配置项包括模型设置、数据库路径、插件目录、权限规则等。下面是一个最小可用的配置示例:
model: provider: "openai" # 可选 openai, ollama, custom base_url: "https://api.openai.com/v1" api_key: "${OPENAI_API_KEY}" model_name: "gpt-6" max_tokens: 4096 temperature: 0.3 memory: sqlite_path: "./data/memory.db" chroma_path: "./data/chroma" max_retrieved_items: 8 plugins: directory: "./plugins" enabled: - "file_ops" - "email" - "calendar" - "knowledge_base" security: require_confirmation_for_write: true allowed_file_paths: - "./workspace" log_redaction: true这里有几个参数我想特别说明。temperature我设成 0.3,因为工作场景需要的是稳定和准确,不是创意。max_retrieved_items设成 8,是我实测下来在准确率和上下文长度之间的最佳平衡点。require_confirmation_for_write一定要开,否则模型可能会在你不知情的情况下修改文件或发送邮件。
4.3 插件开发:以“文件操作”插件为例
插件开发是 OpenWorkMate 最灵活的部分。每个插件就是一个 Python 文件,里面定义了一个继承自BasePlugin的类。下面我以文件操作插件为例,展示核心代码结构:
from openworkmate.plugin import BasePlugin, tool from pydantic import BaseModel, Field from pathlib import Path class ReadFileParams(BaseModel): path: str = Field(..., description="文件路径,相对于工作目录,例如 'docs/report.md'") max_lines: int = Field(100, description="最多读取的行数,默认 100") class FileOpsPlugin(BasePlugin): name = "file_ops" description = "本地文件读写操作" @tool( name="read_file", description="读取本地文件内容。当用户需要查看某个文档时使用。", params_model=ReadFileParams ) async def read_file(self, params: ReadFileParams) -> str: file_path = Path(self.workspace) / params.path if not file_path.exists(): return f"错误:文件 {params.path} 不存在" if not self._is_allowed(file_path): return f"错误:没有权限访问 {params.path}" content = file_path.read_text(encoding="utf-8") lines = content.splitlines()[:params.max_lines] return "\n".join(lines)这里的关键点是:@tool装饰器会自动把函数注册到工具注册表,params_model用 Pydantic 做参数校验,description会直接进入模型的提示词。我试过不写 description,结果模型完全不知道这个工具是干什么的。另外,_is_allowed方法会检查文件路径是否在允许的目录范围内,防止模型访问敏感文件。
4.4 启动与首次对话测试
配置好之后,启动服务很简单:
uvicorn openworkmate.main:app --host 0.0.0.0 --port 8000然后你可以用 curl 或者写一个简单的客户端来测试:
curl -X POST http://localhost:8000/chat \ -H "Content-Type: application/json" \ -d '{"user_id": "test_user", "message": "帮我看看 docs/report.md 里写了什么"}'如果一切正常,你应该能看到模型调用read_file工具,然后返回文件内容。第一次测试的时候,我建议你先用简单的任务,比如“读取文件”、“查询知识库”,确认工具调用链路是通的,再去试复杂的多步任务。
实操心得:第一次启动时,建议把日志级别设成 DEBUG,这样你能看到模型每一步的决策过程。我当初就是靠日志才发现模型把“读取”和“写入”工具搞混了。
5. 常见问题与排查技巧实录
5.1 模型不调用工具,直接瞎编答案
这是最常见的问题。你问它“供应商 A 的报价是多少”,它不调用检索工具,直接编一个数字。原因通常是系统提示词里没有明确要求“必须先检索”,或者工具描述不够清晰。我的解决方法是:在系统提示词里加一条硬规则——“当用户询问任何内部数据时,必须先调用search_knowledge_base工具,不得直接回答”。同时,在工具描述里写清楚“这个工具用于查询内部知识库,包含供应商报价、合同条款、项目进度等信息”。
如果加了规则还是不行,那就检查一下模型的温度参数。温度太高(比如 0.8 以上)会导致模型更倾向于“自由发挥”。我一般把温度设在 0.2 到 0.4 之间。
5.2 工具调用参数传错
比如模型调用read_file时,把path参数传成了file_path,或者把日期格式写成 “March 15” 而不是 “2025-03-15”。这个问题的主要原因是参数描述不够具体。我的做法是在Field的description里加上格式示例,并且在编排器里加一层参数预处理,比如自动把 “March 15” 转换成 “2025-03-15”。
还有一个技巧是在工具函数内部做容错。比如read_file如果收到不存在的路径,不要直接抛异常,而是返回一个友好的错误信息,让模型知道“这个路径不对,请换一个”。这样模型有机会自我纠正。
5.3 记忆检索不准确
有时候模型检索到的记忆和当前问题完全不相关。这通常是向量检索的语义漂移问题。我的解决方法是:第一,用更好的嵌入模型,我试过bge-large-zh比text-embedding-ada-002在中文场景下效果好很多;第二,在检索时加一个时间衰减因子,越近的记忆权重越高;第三,加一个“相关性阈值”,如果检索结果的相似度低于 0.7,就不放进上下文。
还有一个容易被忽略的点是记忆的粒度。如果你把一整段对话作为一个记忆单元,检索出来的东西会很杂。我建议把记忆拆成更小的单元,比如一个事实、一个偏好、一个任务,每个单元单独存储和检索。
5.4 系统响应太慢
如果你用本地模型,响应慢是正常的。70B 模型在消费级 GPU 上,生成 500 个 token 大概需要 20 到 30 秒。我的优化方法是:第一,用流式输出,让用户先看到部分结果;第二,把一些简单的任务(比如意图分类、参数提取)交给小模型处理,比如 7B 的模型,只有复杂推理才用大模型;第三,缓存常见的查询结果,比如“公司通讯录”这种不常变的数据,第一次查完之后缓存起来。
下面是我整理的一个常见问题速查表,你可以直接对照排查:
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 模型不调用工具 | 提示词未强制要求 | 检查系统提示词 | 加硬规则“必须先检索” |
| 参数传错 | 描述不清晰 | 查看工具调用日志 | 补充格式示例,加参数预处理 |
| 记忆检索不准 | 嵌入模型不合适 | 检查检索相似度分数 | 换嵌入模型,加时间衰减 |
| 响应太慢 | 模型太大 | 查看推理耗时 | 流式输出,小模型分流 |
| 工具调用失败 | 权限不足 | 检查权限规则 | 调整 allowed_paths 或角色权限 |
| 模型胡编答案 | 温度太高 | 检查 temperature 参数 | 降到 0.2-0.4 |
5.5 权限控制被绕过
这是一个安全问题,必须重视。我试过模型通过构造特殊的文件路径(比如../../etc/passwd)来尝试访问工作目录之外的文件。解决方法是在_is_allowed方法里做路径规范化,用Path.resolve()把路径转成绝对路径,然后检查它是否在允许的目录之下。另外,对于所有写操作,都必须经过用户确认,不能由模型自主决定。
注意:权限控制一定要在代码层面硬编码,不能依赖模型的“自觉”。模型可能会被诱导,但代码不会。
6. 一些关于“AI 工作伙伴”的实在话
我做 OpenWorkMate 这段时间,最大的体会是:AI 工作伙伴的价值不在于它有多聪明,而在于它有多可靠。一个能稳定完成 10 个任务的 AI,比一个能完成 100 个任务但经常出错的 AI 有用得多。所以我在设计的时候,宁可功能少一点,也要保证每个功能都是经过充分测试的。
另一个体会是,提示词工程不是玄学,是工程。你需要像写代码一样写提示词,有版本管理,有测试用例,有回归测试。我专门建了一个测试集,里面有 50 个典型任务,每次修改提示词或工具描述,都会跑一遍测试集,看看准确率有没有下降。
最后说一个我踩过的最大的坑:不要试图让 AI 替代人做决策。OpenWorkMate 的定位是“助手”,不是“代理”。它可以帮你整理信息、提醒你该做什么、执行你明确授权的操作,但最终的决策必须由人来做。我在系统里加了一个“决策确认”机制,任何涉及金额、合同、人事的操作,都必须由用户手动确认。这个机制救过我一次——模型差点把一个测试环境的报价单发给客户,幸好确认环节拦住了。
如果你也想动手做一个自己的 AI 工作伙伴,我的建议是从最小的场景开始。不要一上来就搞“全能助手”,先做一个“会议纪要整理助手”或者“合同条款查询助手”,把工具调用、记忆存储、权限控制这些基础链路跑通,然后再逐步扩展。OpenWorkMate 的代码我已经整理成开源项目,你可以直接拿去改,也可以只参考架构设计,用你熟悉的技术栈重新实现。关键是动手做,哪怕只做一个能查内部文档的小工具,也比停留在“想”的阶段强。