用 Claude API 搭建本地助手:命令行、历史存储与 Web 界面
从单用户本地原型开始,复用对话核心,安全保存历史,再逐项检查 Web 和容器部署边界。
本地界面不等于离线模型
本地助手把界面和历史放在你的机器,调用 Claude API 时仍会把请求内容发给服务商。不要将“本地运行”宣传为数据不出设备。开始前确定哪些材料可以发送、历史保存多久以及谁能使用这台机器;密钥留在后端环境,聊天历史里不放密钥。
推荐拆成对话核心、CLI、Web 适配和存储四层。第一版只处理文本和单用户,先验证“提问→回复→保存→重启恢复”,再加入文件、工具和多用户。角色设定通过独立 system 配置,模型与输出预算由应用管理。
只保存确认完成的对话
例:制作自己的技术学习助手。用户先问“什么是事务”,再要求“给一个转账例子”;历史应保留这两个问题与完整回答。遇到截断则提示未完成,不自动覆盖旧会话。涉及工具或思考的完整内容块,应依 SDK 的序列化和回放要求保存,不能粗暴丢成纯文本。
文本原型的持久化核心
下面仅支持 user/assistant 纯文本消息,拒绝其他结构;文件由本地应用确定,不采用浏览器传来的任意路径。临时文件与目标文件在同一目录,成功后原子替换。私密目录、文件权限、加密与备份由部署环境负责,JSON 文件本身不提供访问控制。
import json, os, tempfile
from pathlib import Path
def validate_history(data):
if not isinstance(data, list):
raise ValueError("history_must_be_list")
for item in data:
if (not isinstance(item, dict) or set(item) != {"role","content"}
or item["role"] not in {"user","assistant"}
or not isinstance(item["content"], str)):
raise ValueError("invalid_text_history")
return data
def save_history(path, messages):
payload = json.dumps(validate_history(messages), ensure_ascii=False)
path = Path(path)
path.parent.mkdir(parents=True, exist_ok=True)
temp_name = None
try:
with tempfile.NamedTemporaryFile(mode="w", encoding="utf-8", dir=path.parent,
delete=False) as handle:
temp_name = handle.name
handle.write(payload)
handle.flush()
os.fsync(handle.fileno())
os.replace(temp_name, path)
finally:
if temp_name and os.path.exists(temp_name):
os.unlink(temp_name)
def load_history(path):
path = Path(path)
return validate_history(json.loads(path.read_text(encoding="utf-8"))) if path.exists() else []命令行核心如何接上
CLI 循环读取输入,/exit 结束,其他文本交给共享的对话函数;按 type 提取文本并检查 stop_reason,成功后才保存。使用流式时先展示临时片段,拿到最终消息再提交历史。输入为空、Ctrl+C 或网络失败应安全退出当前轮,不能把半条答案保存成完整答复。
历史会随着轮数增加而增加输入量。设置清晰的会话重置入口和大小预算;必要时保留关键目标与最近相关证据,再用摘要替换早期闲聊。不要声称某个大上下文让日常使用永远不会碰到限制。
Web 版的最小安全显示
浏览器只请求自己的后端 /chat,读取响应后用 textContent 展示,避免模型输出变成可执行 HTML。下面是页面已有 input、button、pre 元素时的事件处理片段。Web 服务应验证来源/主机,并按访问范围启用认证;本地回环监听并不免除浏览器安全检查。
document.querySelector("button").addEventListener("click", async () => {
const output = document.querySelector("pre");
output.textContent = "正在处理…";
try {
const response = await fetch("/chat", {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ text: document.querySelector("input").value })
});
if (!response.ok) throw new Error("request_failed");
const data = await response.json();
output.textContent = data.text + (data.stop_reason === "end_turn" ? "" : "\n[未确认完整]");
} catch { output.textContent = "请求未完成,请检查后端状态。"; }
});部署与验收
本地启动只绑定 127.0.0.1;需要他人访问时重新设计 TLS、账号与会话所有权。容器镜像只包含程序和锁定依赖,密钥由运行环境注入,历史使用受控持久卷;不要把 .env 复制进镜像。多进程并发写历史时应换数据库或可靠锁。
验收包括重启恢复、异常不污染历史、空输入拒绝、XSS 文本安全显示、历史删除和费用统计。本文验证本地存储与语法,没有发布服务或连接 API;Web/容器层需按自己的环境联调后才能开放。
参考来源
资料核对日期:2026-10-04。涉及产品与账户条件时,请以当前官方说明为准。