最近在 Hacker News 的 Show HN 板块看到一个很有意思的项目 Manner,一句话描述就是:开发者创建 AI 克隆,客户可以像雇佣员工一样使用它们。这个定位让“AI 代理”从企业自建工具,变成了一种可以打包交付、按岗位雇佣的数字劳动力。
很多读者看到“AI 克隆”可能会先想到人脸、声音的深度伪造。这里先明确边界:本文不讨论 Deepfake 方向,而是专注于基于大语言模型(LLM)的角色化智能代理。“AI 克隆”在工程语境下通常指:让大模型具备某个特定岗位的人设、技能、记忆和工具调用能力,比如销售助理、客服专员、技术支持、招聘初筛等。
从架构师和后端开发者的角度看,一个能交付给客户“雇佣”的 AI 克隆系统到底由哪些核心模块组成?如果我们要从零实现一个最小可用版本,应该怎么设计?生产落地时又有哪些容易被忽略的坑?这篇文章会把整个链路拆开讲清楚,并附上可运行的代码示例。
1. 背景与核心概念
1.1 Manner 这类产品到底在解决什么问题
过去几年,大家都在做聊天机器人。但大多数聊天机器人仍然是“你说一句,我回一句”的问答工具,缺乏明确的岗位身份、任务边界和交付标准。客户即使接入了一个 ChatBot,也很难说清楚它到底能替员工完成哪部分工作。
Manner 这类产品提出了一个新的抽象:把 AI 当作“可雇佣的人”。开发者像写员工说明书一样定义 AI 的性格、职责、可用工具和知识范围;客户则像招人一样挑选、激活、使用这些 AI 克隆。这个模型的价值在于:
- 交付物从“接口”变成了“角色”;
- 客户不再关心底层是哪个大模型,只关心它是否胜任某个岗位;
- 开发者可以通过“克隆模板”批量复制相似角色,降低定制成本。
本质上,这是把大模型应用产品化、岗位化的一种尝试。它解决的痛点不是“AI 能不能回答问题”,而是“AI 能不能稳定地承担一个具体岗位的工作”。
1.2 AI 克隆与普通聊天机器人的区别
普通聊天机器人通常是无状态的问答系统,核心链路是:
用户输入 -> 大模型生成 -> 返回文本而 AI 克隆的完整链路要复杂得多:
用户输入 -> 识别意图 -> 携带记忆 -> 调用人设 -> 检索知识 -> 执行工具 -> 生成回复 -> 更新记忆二者在以下几个维度有明显差异:
| 维度 | 普通聊天机器人 | AI 克隆 |
|---|---|---|
| 人设 | 无,通用问答 | 有明确的角色、语气、岗位边界 |
| 记忆 | 通常无状态 | 需要跨会话记忆,记住关键上下文 |
| 工具 | 不支持 | 可查询库存、创建工单、发送通知 |
| 知识 | 依赖模型内部知识 | 可接入私有知识库做 RAG |
| 交付方式 | 一个 API | 一个可部署、可审计的数字员工 |
这也是为什么 AI 克隆不能简单用“一个 Prompt + 一个模型 API”来实现,它需要有人设管理、记忆管理、工具注册和服务化封装这几层工程支撑。
1.3 典型应用场景
以“可雇佣”的角度看,适合用 AI 克隆承接的岗位通常具备以下特征:流程相对标准化、历史记录可复用、决策边界清晰。常见场景包括:
- 销售助理:跟进线索、回答产品参数、创建跟进记录;
- 客服专员:处理退换货、查询订单、转人工;
- 技术支持:根据文档排查问题、生成诊断报告;
- 招聘助理:初筛简历、预约面试、回复候选人;
- 运营助手:定时整理数据、生成周报草稿。
这类岗位的共同点是:它们消耗了大量人力,但工作内容并不需要真正的“人类创造力”。AI 克隆可以先承接 80% 的重复劳动,再由人类处理剩余需要判断的 20%。
2. AI 克隆的技术架构
2.1 整体分层
一个工程上可用的 AI 克隆系统,可以按四层来设计:
第一层是接入层。负责接收客户请求,包括 Web、企业微信、钉钉、Slack 等渠道。接入层主要做身份认证、频率控制、参数校验。
第二层是 AI 编排层。这是整个系统的核心。它负责加载人设、组合上下文、判断是否需要调用工具、调用大模型生成回复。编排层要解决的是“什么时候聊天、什么时候检索、什么时候调用 API”。
第三层是工具层。AI 克隆要真正干活,必须能与外部系统交互。常见工具包括商品查询接口、工单系统、CRM、日历、邮件等。工具层把外部能力封装成函数,并暴露给大模型。
第四层是数据层。包括记忆存储、知识库、日志和审计数据。数据层决定了 AI 克隆是否“记得住”和“懂业务”。
2.2 人设:AI 克隆的“性格和岗位说明书”
人设是整个 AI 克隆最容易被低估的部分。很多人以为人设就是一句“你是一个友好的客服”,实际上,一份可工程化的人设应该包含:
- 角色定位:AI 是谁,在哪个岗位;
- 职责边界:哪些事情必须做,哪些事情坚决不做;
- 话术风格:语气、长度、是否使用 emoji;
- 敏感行为:被问到竞品、价格、法律纠纷时如何回应;
- 兜底策略:不确定时如何请求转人工或提供联系方式。
人设建议以 YAML 或 JSON 文件管理,而不是硬编码在业务代码里。这样可以直接把“人设文件”作为交付物给客户审阅,也方便灰度测试不同人设效果。
2.3 记忆:让克隆记住来龙去脉
记忆是 AI 克隆与普通 API 调用的最大区别。工程上记忆分为两类:
短期记忆。指当前会话中最近几轮对话,一般直接拼进 Prompt。短期记忆最容易出现的问题是超出模型上下文窗口,因此需要做截断或摘要。
长期记忆。指跨会话的关键信息,比如客户名称、偏好、历史工单编号。长期记忆通常存储在 Redis、MySQL 或向量数据库中,在对话开始时按用户 ID 加载。
实现记忆的最小方案是内存字典加超时清理,适合 demo;生产环境则需要持久化存储,并设置记忆的保留和清理策略。
2.4 工具调用与知识库:从“能聊”到“能干活”
一个只会聊天的 AI 克隆价值有限。真正让它胜任岗位的是两件事:
工具调用。大模型根据用户意图输出一个结构化调用指令,系统执行外部 API 后把结果返回给模型继续生成回复。这一机制在 OpenAI 的 Function Calling 以及 Anthropic 的 Tool Use 中已经有标准实现。
知识库检索(RAG)。把客户提供的产品手册、FAQ、政策文档切片后做向量化存储。对话时先根据用户问题检索最相关的知识片段,再让模型基于这些片段回答。这样可以显著减少幻觉,且知识更新不需要重新微调模型。
3. 环境准备与项目结构
3.1 开发环境
本文示例以 Python 3.10+ 为基础。需要准备:
- Python 3.10 或更高版本;
- 一个支持 Tool Calling/Function Calling 的 LLM API,示例代码使用 OpenAI 兼容协议;
- FastAPI 和 Uvicorn 用于搭建服务;
- 向量检索用简单的 numpy 余弦相似度代替,避免引入过重的基础设施。
如果你的模型服务商不支持 tools 参数,可以把第 4.4 节的工具调用逻辑替换成“JSON 输出解析”方式,整体思路不变。
3.2 项目目录结构
我建议按下面的结构组织代码:
manner-clone/ ├── app/ │ ├── __init__.py │ ├── main.py # FastAPI 入口 │ ├── agent.py # AI 编排核心逻辑 │ ├── memory.py # 记忆管理 │ ├── tools.py # 工具函数注册 │ ├── personas/ │ │ └── sales.yaml # 销售助理人设 │ └── knowledge/ │ └── products.txt # 产品知识库明文 ├── requirements.txt └── .env这种分层方式的好处是:人设、工具、知识库彼此解耦,后续可以独立演进。
3.3 依赖安装
创建虚拟环境并安装依赖:
python -m venv .venv source .venv/bin/activate # Windows 下使用 .venv\Scripts\activate pip install -r requirements.txtrequirements.txt 内容如下:
openai>=1.30.0 fastapi>=0.110.0 uvicorn[standard]>=0.29.0 python-dotenv>=1.0.0 PyYAML>=6.0.0 numpy>=1.26.0 pydantic>=2.6.03.4 配置文件
在项目根目录创建 .env 文件,把 API Key 等敏感信息放在环境变量中:
LLM_API_KEY=sk-your-key LLM_BASE_URL=https://api.your-llm-provider.com/v1 LLM_MODEL=your-chat-model强烈建议将 .env 加入 .gitignore,避免密钥泄漏。
4. 完整实战:从零实现一个可雇佣的销售助理克隆
下面我们以一个“销售助理”克隆为例,完成从人设到服务化的完整实现。
4.1 定义人设文件
人设文件放在 app/personas/sales.yaml:
name: 小莫 role: 销售助理 description: 负责产品介绍、库存查询、线索登记,不负责价格谈判和合同签署。 style: language: zh-CN tone: 专业且友好 max_length: 200 boundaries: - 不承诺折扣 - 不讨论竞品优劣 - 用户要求转人工时,提供联系方式并结束对话 - 用户询问无法回答的问题时,说明需要人工介入 tools: - query_stock - create_lead人设文件是 AI 克隆的“岗位说明书”,它比 System Prompt 更结构化,便于管理和复用。
4.2 编写记忆模块
先实现一个简单的记忆管理器,用内存字典保存会话记录,并按用户读取。文件路径:app/memory.py
import time from collections import defaultdict, deque from typing import Dict, List class MemoryStore: def __init__(self, max_turns: int = 20, ttl_seconds: int = 3600): self.max_turns = max_turns self.ttl_seconds = ttl_seconds self._history: Dict[str, deque] = defaultdict(deque) self._updated_at: Dict[str, float] = {} def _expire(self, user_id: str): ts = self._updated_at.get(user_id, 0) if time.time() - ts > self.ttl_seconds: self._history[user_id].clear() def add(self, user_id: str, role: str, content: str): self._expire(user_id) self._history[user_id].append({"role": role, "content": content}) if len(self._history[user_id]) > self.max_turns: self._history[user_id].popleft() self._updated_at[user_id] = time.time() def get(self, user_id: str) -> List[dict]: self._expire(user_id) return list(self._history[user_id]) def clear(self, user_id: str): self._history[user_id].clear()生产环境中,建议使用 Redis 或 MySQL 替代内存存储,尤其当服务需要多实例部署时。这个示例关注的是结构,而不是存储方案。
4.3 注册工具函数
文件路径:app/tools.py
import json import datetime def query_stock(sku_id: str) -> str: """模拟查询商品库存""" stock_map = { "SKU-1001": {"name": "无线鼠标", "stock": 23}, "SKU-1002": {"name": "机械键盘", "stock": 5}, "SKU-1003": {"name": "显示器支架", "stock": 0}, } item = stock_map.get(sku_id) if not item: return json.dumps({"error": "SKU not found"}, ensure_ascii=False) return json.dumps(item, ensure_ascii=False) def create_lead(name: str, phone: str, product_interest: str) -> str: """创建销售线索,模拟写入 CRM""" lead_id = "LEAD-" + datetime.datetime.now().strftime("%Y%m%d%H%M%S") return json.dumps({ "lead_id": lead_id, "name": name, "phone": phone, "product_interest": product_interest, "status": "created" }, ensure_ascii=False) TOOL_MAP = { "query_stock": query_stock, "create_lead": create_lead, }注意:工具函数返回的是 JSON 字符串,便于大模型理解。这里没有真正连接数据库和 CRM,实际项目中只需要替换函数内部逻辑即可。
4.4 AI 对话主循环
文件路径:app/agent.py。这是整个系统最核心的部分。
import json import os from typing import List import yaml from openai import OpenAI from memory import MemoryStore from tools import TOOL_MAP with open("app/personas/sales.yaml", "r", encoding="utf-8") as f: PERSONA = yaml.safe_load(f) SYSTEM_PROMPT = f""" 你是{ PERSONA['name'] },岗位是{ PERSONA['role'] }。 角色描述:{ PERSONA['description'] } 沟通风格:{ PERSONA['style']['tone'] },回复尽量控制在{ PERSONA['style']['max_length'] }字以内。 岗位边界: { chr(10).join('- ' + b for b in PERSONA['boundaries']) } """ memory_store = MemoryStore() client = OpenAI( api_key=os.getenv("LLM_API_KEY"), base_url=os.getenv("LLM_BASE_URL"), ) MODEL = os.getenv("LLM_MODEL", "gpt-4o-mini") def build_tools(): return [ { "type": "function", "function": { "name": "query_stock", "description": "查询指定SKU的库存情况", "parameters": { "type": "object", "properties": { "sku_id": {"type": "string", "description": "商品SKU编号"} }, "required": ["sku_id"] } } }, { "type": "function", "function": { "name": "create_lead", "description": "创建一条销售线索", "parameters": { "type": "object", "properties": { "name": {"type": "string", "description": "客户姓名"}, "phone": {"type": "string", "description": "联系电话"}, "product_interest": {"type": "string", "description": "感兴趣的产品"} }, "required": ["name", "phone", "product_interest"] } } } ] def run_tool_call(tool_name: str, arguments: dict) -> str: func = TOOL_MAP.get(tool_name) if not func: return json.dumps({"error": f"unknown tool: {tool_name}"}) return func(**arguments) def chat(user_id: str, user_message: str) -> str: history = memory_store.get(user_id) messages: List[dict] = [{"role": "system", "content": SYSTEM_PROMPT}] messages.extend(history) messages.append({"role": "user", "content": user_message}) # 工具调用最多循环 5 次,防止死循环 for _ in range(5): response = client.chat.completions.create( model=MODEL, messages=messages, tools=build_tools(), tool_choice="auto", ) choice = response.choices[0].message if not choice.tool_calls: # 没有工具调用,直接返回 reply = choice.content or "" memory_store.add(user_id, "user", user_message) memory_store.add(user_id, "assistant", reply) return reply # 先把工具调用结果追加到消息列表 messages.append({ "role": "assistant", "content": choice.content, "tool_calls": [ { "id": tc.id, "type": "function", "function": { "name": tc.function.name, "arguments": tc.function.arguments } } for tc in choice.tool_calls ] }) for tc in choice.tool_calls: args = json.loads(tc.function.arguments) result = run_tool_call(tc.function.name, args) messages.append({ "role": "tool", "tool_call_id": tc.id, "content": result }) return "处理超时,请联系人工客服。"这里的核心是“模型判断-执行工具-回填结果-再次生成”的循环。工具调用并不是让模型直接访问数据库,而是由系统安全地执行外部函数,再把结果交给模型组织语言。
4.5 封装 FastAPI 服务
文件路径:app/main.py
from fastapi import FastAPI, HTTPException from pydantic import BaseModel from agent import chat app = FastAPI(title="Manner Clone API") class ChatRequest(BaseModel): user_id: str message: str class ChatResponse(BaseModel): reply: str @app.post("/chat", response_model=ChatResponse) async def chat_endpoint(req: ChatRequest): if not req.user_id.strip(): raise HTTPException(status_code=400, detail="user_id is required") if not req.message.strip(): raise HTTPException(status_code=400, detail="message is required") reply = chat(req.user_id, req.message) return ChatResponse(reply=reply)这里使用 user_id 区分不同客户,每个客户拥有独立的记忆上下文。生产环境还需要加入 API Key 鉴权和限流,避免服务被滥用。
4.6 运行与验证
启动服务:
uvicorn app.main:app --reload --port 8000然后通过 curl 测试:
curl -X POST http://localhost:8000/chat \ -H "Content-Type: application/json" \ -d '{"user_id": "customer_001", "message": "你好,请问 SKU-1001 有货吗?"}'预期返回中,模型会判断需要调用 query_stock 工具,拿到库存结果后生成类似这样的回复:
{ "reply": "SKU-1001(无线鼠标)目前有 23 件库存,可以正常下单。需要帮您登记一下购买意向吗?" }这个最小系统已经具备人设、记忆、工具调用和服务化四层能力。你可以在此基础上扩展更多岗位克隆。
5. 进阶:RAG 让克隆掌握客户私有资料
5.1 为什么需要 RAG
大模型能回答通用问题,但不知道客户私有的产品手册、服务条款和内部流程。如果直接把所有资料塞进 Prompt,很快会超出上下文窗口,而且知识更新成本高。RAG 的解决思路是:平时把资料向量化存储,对话时只检索用户问题最相关的片段,再让模型基于这些片段回答。
5.2 索引与向量化
为了简化示例,我们使用 numpy 手动实现余弦相似度检索,不引入重型的向量数据库。文件路径:app/knowledge.py
import os import re import numpy as np from openai import OpenAI client = OpenAI( api_key=os.getenv("LLM_API_KEY"), base_url=os.getenv("LLM_BASE_URL"), ) EMBEDDING_MODEL = os.getenv("EMBEDDING_MODEL", "text-embedding-3-small") def load_documents(path: str) -> list[str]: with open(path, "r", encoding="utf-8") as f: text = f.read() # 简单按段落切分 paragraphs = [p.strip() for p in re.split(r"\n\s*\n", text) if p.strip()] return paragraphs def embed_texts(texts: list[str]) -> np.ndarray: resp = client.embeddings.create(model=EMBEDDING_MODEL, input=texts) vectors = [d.embedding for d in resp.data] return np.array(vectors, dtype=np.float32) class SimpleVectorStore: def __init__(self): self.docs: list[str] = [] self.vectors: np.ndarray | None = None def build(self, docs: list[str]): self.docs = docs self.vectors = embed_texts(docs) norm = np.linalg.norm(self.vectors, axis=1, keepdims=True) self.vectors = self.vectors / norm def search(self, query: str, top_k: int = 2) -> list[str]: q_vec = embed_texts([query])[0] q_norm = np.linalg.norm(q_vec) q_vec = q_vec / q_norm scores = self.vectors @ q_vec top_indices = scores.argsort()[-top_k:][::-1] return [self.docs[i] for i in top_indices]实际的 RAG 系统还包含去重、重排序、元数据过滤等步骤。但上面的示例已经能让 AI 克隆拥有“读取文本资料”的能力。
5.3 检索增强对话示例
把检索结果注入到对话消息中,让模型基于知识片段回答。在 agent.py 的 chat 函数中增加如下步骤:
# 在 append user_message 之前插入知识片段 if user_message.strip(): knowledge = vector_store.search(user_message, top_k=2) knowledge_text = "\n\n".join(knowledge) messages.append({ "role": "system", "content": f"请优先参考以下内部资料回答用户问题:\n{knowledge_text}\n如果资料中没有答案,明确告诉用户需要人工介入。" })为了避免资料内容被用户套取,建议在知识注入时增加一条指令:“不要原文输出内部资料”。RAG 的注入顺序、检索阈值、片段数量都会影响最终效果,需要根据实际数据调优。
6. 常见问题与排查思路
6.1 高频问题表
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| 工具调用总是返回空 | 模型不支持 Function Calling | 更换支持 tools 参数的模型,或改为 JSON 输出解析 |
| 回答内容偏长、偏啰嗦 | 人设文件约束不足 | 在 style.max_length 中降低限制,并在 System Prompt 中加强约束 |
| 记忆串号 | 使用了错误的 user_id 作为 key | 检查接入层是否正确传递用户标识 |
| 总是说不知道 | 知识库没有命中相关片段 | 调低检索阈值,或优化文本切分方式 |
| 超过上下文长度 | 记忆列表过长 | 启用摘要功能,或减小 max_turns |
| API 调用成本过高 | 工具循环次数过多 | 限制最大循环次数,增加结果缓存 |
| 用户套取系统提示词 | 缺少防注入处理 | 在 System Prompt 中加入拒绝规则,并做输入过滤 |
6.2 典型排查案例
案例 1:模型明明返回了 tool_calls,但系统没有执行工具。
先检查是否把 response.choices[0].message 完整追加到了 messages 中。许多实现漏掉了 assistant 这条带 tool_calls 的消息,导致模型无法获知工具执行结果。
案例 2:多个工具同时被调用,但最终回复内容错乱。
需要遍历 tool_calls,逐个执行,并保证每个 tool 的结果对应的 tool_call_id 正确。一个常见错误是自行构造 tool_call_id,导致关联失败。
案例 3:RAG 检索结果不相关。
可以先把检索到的知识片段单独打印出来,确认片段本身是否相关。如果片段不相关,问题通常出在文本切分,而不是模型。
7. 最佳实践与工程建议
7.1 安全与合规边界
AI 克隆让大模型代表企业直接对外服务,安全边界必须提前画清楚。
一是权限隔离。工具层必须使用服务账号,不能使用有大数据权限的员工个人凭证。例如查询库存只暴露必要字段,不允许调用内部管理接口。
二是防提示注入。用户可能在对话中试图让 AI 忽略系统指令。建议在 System Prompt 中增加防御性约束,并对用户输入中的控制指令进行识别。
三是隐私保护。涉及个人信息时,需要遵循最小化原则。生产环境应脱敏展示电话、地址等敏感数据,并设置日志脱敏策略。
四是合规授权。客户知识库中包含的文档、政策,必须确认企业有合法使用和对外展示的授权。不要在未授权情况下让 AI 克隆背诵或转述内部资料。
7.2 成本控制
大模型成本主要来自三点:输入 Token、输出 Token 和 Embedding 调用。
建议限制用户单次请求的最大 Token 数,启用频率限制。对常用问题可增加本地缓存,避免重复调用模型。工具调用结果尽量结构化返回,减少模型重复推理的次数。
记忆管理也很关键。每轮对话都会把历史记录发送给模型,随着记录变长,成本会快速增长。建议超过固定轮数后,将早期对话压缩成摘要,而不是保留完整原文。
7.3 质量评估与灰度发布
AI 克隆上线前,需要建立一套评估集。收集 100 到 200 条典型用户问题,标注期望行为(直接回答、调用工具、转人工),然后逐个测试克隆的表现。每次修改人设或知识库后,都回归测试这组问题。
上线过程建议灰度:先开放给少量客户试用,观察命中率、转人工率、平均对话轮数等指标,稳定后再全量开放。不要把生产环境的 Prompt 改动当成一次普通配置修改,它可能改变整个岗位的行为。
7.4 可观测性
给 AI 克隆加日志时,至少记录四类信息:
- 用户问题原文和脱敏后的用户标识;
- 最终发给模型的 Prompt 片段,方便排查人设和知识注入问题;
- 工具调用记录,包括名称、参数、返回结果和耗时;
- 模型回复以及延迟、Token 用量。
如果某个客户反馈“AI 乱说话”,没有日志就无法定位是知识库错误、提示词被注入,还是工具返回错误数据。可观测性是承接“雇佣”类业务的底线能力。
8. 总结
从 Manner 这个 Show HN 项目出发,我们拆解了一个 AI 克隆系统需要的核心能力:人设管理、记忆、工具调用、知识库检索和服务化封装。文中提供了一套可运行的最小实现,覆盖了从 YAML 人设到 FastAPI 服务发布的完整链路。
如果你打算自己做一个类似产品,我建议按下面的优先级推进:
第一步,先把单角色跑通。不要一开始就做多角色管理平台,先用一个销售助理或客服专员验证技术链路。
第二步,完善工具和知识库。人设决定体验上限,工具和知识决定业务价值。先接入一两个真实系统,比如 CRM、工单系统。
第三步,再加管理面。当你有 10 个以上的 AI 克隆实例时,才需要考虑模板管理、权限控制、用量统计和灰度发布。
AI 克隆真正难的地方并不是调大模型的接口,而是如何把模糊的岗位职责翻译成精确的人设、工具和边界,并保证它在生产环境中稳定、可控、可审计。这个方向还在快速演进,如果你的业务刚好有一些重复性较高的服务岗位,可以用上面的思路做一个最小验证,跑通后再逐步扩展。