1. 为什么要把名人“做成分身”:一个关键词拆解思路
先说个我自己的感受。每次刷到各类人物访谈,总有读者在评论区问:“如果能直接问本人就好了”,“某某大师要是能回答我的问题该多好”。这个需求听起来像科幻,但用大模型技术拆解一下,其实就是两件事:
第一,让AI能够模仿特定人物的语言风格、知识范围和思维习惯;第二,让AI在遇到超出“本人”知识边界的问题时,还能通过实时搜索补充信息,而不是一本正经地胡说八道。把这两件事串起来,就需要一个能编排模型和外部工具的框架。
我选的是Langchain框架,搭配百度搜索API,做一个“名人数字分身”的MVP。为什么这么选?
- Langchain是目前最成熟的Agent编排框架,内置了工具调用、记忆管理、输出解析这一整套流水线,省去自己写胶水代码的时间;
- 百度搜索API在国内访问速度稳定,申请门槛低,适合快速验证想法;
- 名人本身是公开人物,语料充足,的人格设定不会涉及隐私问题,方便做技术验证。
这篇文章是系列的第一篇,只做一件核心事:把Langchain、大模型、百度搜索API三者串起来,跑通一个“能回答、能搜索、有角色感”的数字分身雏形。后续再在这个基础上做长期记忆、多轮对话优化、声音克隆、超写实渲染等进阶功能。
如果你是第一次接触Agent开发,这篇文章能帮你搞清“大模型 + 搜索工具”到底是怎么协作的;如果你已经写过不少Langchain代码,那我的踩坑记录和参数调优思路也许能给你一些参考。
2. 从“角色扮演”到“Agent工作流”:底层原理拆解
很多初学者上来就写代码,写完Prompt就跑,结果发现分身说话一会儿像本人,一会儿又像聊天机器人,问个实时新闻还瞎编。问题不在模型,在架构。
2.1 数字分身的三层分工
一个合格的数字分身,我理解应该有清晰的三层结构:
第一层是角色层。它负责“像谁”。这块最关键的是角色设定,包括人物的生平经历、观点立场、语言习惯、回答风格。现在的做法是把这部分写成System Prompt,通过精心设计的提示词注入给大模型。后面我会给出一个可以直接改的模板。
第二层是工具层。它负责“能做什么”。数字分身不能只靠模型内部知识硬扛,遇到时效性问题、具体数据查询、社会新闻,都必须借助外部搜索。工具层把百度搜索API封装成Langchain可以调用的工具,让大模型自主决定“这个问题我要不要搜一下”。
第三层是编排层。它负责“怎么配合”。大模型先判断用户意图,决定是否调用工具;如果需要,就触发搜索,拿到结果后理解、整合,再用人物的口吻回答。这一整套循环就是Agent机制。Langchain在这里做的就是把“思考-行动-观察-再思考”的循环流程标准化。
这三层关系可以类比成一个真人助理:角色层是助理的“人设”,工具层是助理手里的“资料库”,编排层是助理“动脑判断什么时候该翻资料”的决策过程。
2.2 Langchain ReAct Agent的核心机制
Langchain实现Agent最常用的是ReAct范式。ReAct这个词是Reasoning和Acting的组合,核心思想非常简单:让模型把思考过程说出来,然后根据思考决定行动,看到行动结果后再继续思考。
具体到我们这个场景,一轮完整的Agent循环是:
- 用户问:“某位演员最近的票房成绩怎么样?”
- 模型收到问题,结合System Prompt里的人设,先在内部生成一段思考(通常不可见,但对开发者调试很有用):
- “这个问题涉及近期票房数据,我的训练知识可能过时了,需要查询。”
- 模型发起一次工具调用(Action),调用百度搜索API,传入查询词;
- 等待搜索工具返回结果(Observation);
- 模型“看到”搜索结果后,重新组织语言,结合人物风格生成最终回答。
这个循环在Langchain里由AgentExecutor驱动,内置了最大迭代次数上限,防止模型陷入死循环。我把这个迭代次数设为5,实测下来对于普通问答足够,遇到复杂问题偶尔需要调高到8,但太高容易出现模型思维发散、跑偏主题的情况。
2.3 为什么选百度搜索API而不是直接用什么Browser工具
不少教程会用Selenium抓网页或者用某种通用搜索替代方案,但我坚持用百度搜索API的官方接口,原因有二。
第一个原因是合规和稳定性。直接用爬虫抓取搜索结果页,随时可能被反爬策略拦掉,而且抓下来的HTML要自己清洗,麻烦不说,还有风险。官方API返回的是结构化JSON,字段干净,还有配额管理,适合做MVP,后续也可以平滑迁移到企业版。
第二个原因是响应速度。数字分身是要给用户实时对话的,搜索响应时间直接决定了“分身”回复的体感速度。百度搜索API的响应基本在几百毫秒到一两秒之间,配合大模型生成时间,总耗时能控制在3秒左右,在聊天场景里是可以接受的。如果是自己爬搜索引擎,加上反爬重试、页面渲染,时间翻倍都不止。
3. 环境准备与API接入:容易被小坑绊倒的地方
这块看起来是照文档填Key就行,但我实际跑的时候还是折腾了小半天,主要是三个点:依赖版本、API请求参数、超时处理。
3.1 初始环境配置
我的环境如下,仅供参考,不需要完全一致:
- Python:3.10.12
- langchain:0.1.0
- langchain-community:0.0.10
- openai:1.6.1(这里顺便说一句,你可以用OpenAI兼容接口,也可以用国内各家大模型的开放平台,只要接口风格兼容OpenAI格式,Langchain都能直接接)
- requests:2.31.0
依赖安装命令很简单:
pip install langchain langchain-community openai requests但这里有个需要注意的点:Langchain这个框架迭代极快,API变动频繁。我最初参考的老教程里很多写法已经过时了,例如把搜索工具直接写在langchain包里,老代码是from langchain.tools import BaiduSearch,现在要先从langchain_community.utilities import BaiduSearchAPIWrapper,再通过langchain.tools.Tool进行封装。如果你跑代码遇到ModuleNotFoundError,优先检查是不是导入路径变了。
3.2 百度搜索API的申请与调用细节
百度搜索API的申请入口在百度智能云控制台,流程就是注册账号、登录、找到搜索服务、创建应用、拿到API Key和Secret Key。门槛不高,但对第一次操作的朋友有两个细节提醒:
第一,个人认证就能用基础配额,不需要企业认证。第二,API Key和Secret Key是两个东西,调用时要用它们换取Access Token,而不是直接用API Key调接口。这一步很多人会忽略,导致鉴权失败,报错信息也看不明白。
换Access Token的代码很简单:
import requests def get_access_token(api_key, secret_key): url = "https://aip.baidubce.com/oauth/2.0/token" params = { "grant_type": "client_credentials", "client_id": api_key, "client_secret": secret_key } resp = requests.post(url, params=params) return resp.json().get("access_token")换了Access Token之后才能拿去查搜索接口。我这里把它封装成一个get_baidu_search_results(query, token)函数,返回前几条搜索结果标题和链接。MVP阶段够用了。
3.3 封装Langchain可识别的搜索工具
Langchain对工具的定义有个统一的接口:工具必须有name、description和_run方法。Description尤其重要,因为大模型就是靠它判断“什么情况下该用我这个工具”。这决定了数字分身在遇到问题时能不能想起来去搜索。
我给搜索工具写的description是按照实际效果打磨过的:
from langchain.tools import Tool def baidu_search_func(query: str) -> str: token = get_access_token(BAIDU_API_KEY, BAIDU_SECRET_KEY) results = get_baidu_search_results(query, token) return format_search_results(results) search_tool = Tool( name="BaiduSearch", func=baidu_search_func, description="当用户询问实时信息、最新新闻、具体数据、人物近况时,必须使用此工具搜索百度后再回答。" )这个description看似随意,其实直接影响Agent的决策行为。我一开始写的是“搜索工具”,结果模型经常在不需要搜索的日常闲聊问题里也去搜索,导致回复很僵硬,上下文也被乱七八糟的搜索结果打断。后来改成“必须使用此工具搜索后再回答”,效果立刻好很多,尤其是涉及时效性问题时。
4. 角色设定与Prompt工程:数字分身的灵魂所在
工具链搭好以后,真正的“人味儿”就全靠Prompt了。Prompt设计得越细,分身的说话风格就越像目标人物;设计得粗糙,模型就自动退回“通用AI客服”模式,整个项目就白干了。
4.1 Persona Prompt模板:不要只写“你扮演某某”
“你扮演某某”这种一句话人设,在早期模型上还能凑合,现在基本没用。模型对人物的理解主要来自训练数据里关于该人物的描述性文本,但那种文本是“第三人称传记体”,不是“第一人称对话体”,所以模仿出来的语气天然就有距离感。
我建议把人设拆成下面几个模块:
你是{名人姓名},一位{身份描述}。 【生平背景】 {一段话介绍对方的成长经历、关键节点、高光时刻} 【性格与语言风格】 {对方是幽默还是严肃?用词偏口语还是偏学术?喜欢用短句还是长句?} 【典型观点】 {列出3-5条该人物在公开场合反复强调的核心观点} 【回答准则】 1. 严格用第一人称回答,不要跳出角色; 2. 不要主动表明自己是AI或语言模型; 3. 如果问题涉及你知识截止日期之后的信息,必须使用BaiduSearch工具查询后再回答; 4. 回答保持口语化,避免清单化输出; 5. 遇到无法回答的问题,像真人一样表明“这个我不太确定”,然后建议搜索或转移话题。为什么“回答准则”这么重要?因为多数模型在被问到超出训练数据的问题时,会条件反射地用通用话术搪塞:“作为AI,我无法回答”。但这条规则一旦写进Prompt,模型就会先尝试调用搜索工具,实在查不到再表明不确定,行为模式更接近真人。
4.2 让角色知识库与搜索互为补充
这里要谈一个常见误区:有人觉得,既然有搜索工具了,是不是Prompt里就不用写太多背景知识了?我用下来的体验是,正好相反。
搜索能解决的是事实性问题,比如“最近怎么样”“今天有什么新闻”这类实时信息。但人物一生的经历、核心思想、典型言论,这些内容搜索反而容易搜出一堆营销号式的概括,并不准确。更关键的是,这些知识决定了分身说话的“底气”。
想象一个场景:用户问“您当年创业最困难的时候是怎么熬过来的”。这个问题的答案不在搜索引擎里,而在人物自传或大量访谈中。如果Prompt里没有这些“生平细节”,模型要么瞎编,要么回答得非常笼统,一听就是AI套话。
所以我的做法是给Prompt配一个“知识附件”,把人物的关键经历和言论写进去。这相当于给模型建了一个专属知识包,和搜索形成互补。实测效果很显著:在没有知识附件时,回答常常是“正确的废话”;有了知识附件以后,回答开始出现具体的时间、地点、事件细节,可信度明显提升。
4.3 SystemPrompt与UserMessage的分工
Langchain中角色设定的核心技术点在于:SystemPrompt负责定义人格和行为规则,UserMessage负责携带用户当前问题。不要试图把用户消息写进SystemPrompt让你替用户说话,那样会让人格和对话逻辑纠缠在一起,修起来很痛苦。
我在代码里是这样组织Prompt结构的:
from langchain.prompts import ChatPromptTemplate, MessagesPlaceholder prompt = ChatPromptTemplate.from_messages([ ("system", persona_prompt), MessagesPlaceholder(variable_name="chat_history"), ("user", "{input}"), MessagesPlaceholder(variable_name="agent_scratchpad") ]) from langchain.memory import ConversationBufferWindowMemory memory = ConversationBufferWindowMemory( memory_key="chat_history", k=6, return_messages=True )这段代码里有几个细节值得说:
MessagesPlaceholder(variable_name="chat_history")是历史对话的插槽,后面接多轮对话时会自动填充;agent_scratchpad是Agent内部思考过程的临时存放区;k=6是记忆窗口大小,控制在6轮对话以内,既能让分身记住近期聊过什么,又不会因为上下文太长拖慢生成速度,还能防止上下文过长导致模型“忘记”人设。
5. 核心实现拆解:Agent串联大模型与搜索工具
架子都搭好了,现在讲主线串联。
我之前之所以强调Langchain的版本兼容性,就是因为Agent的初始化方式变了好多次。当前稳定可用的写法如下:
from langchain.agents import AgentExecutor, create_openai_functions_agent from langchain_openai import ChatOpenAI llm = ChatOpenAI( model="gpt-4o-mini", # 也可以换成国内大模型的OpenAI兼容接口 temperature=0.8, max_tokens=1024, openai_api_base="https://api.openai.com/v1", api_key=OPENAI_API_KEY, ) tools = [search_tool] agent = create_openai_functions_agent(llm, tools, prompt) agent_executor = AgentExecutor( agent=agent, tools=tools, memory=memory, verbose=True, max_iterations=5, handle_parsing_errors=True, )这段代码的每一行基本都有讲究,逐个说明:
create_openai_functions_agent:这是Langchain针对Function Calling类模型设计的专用Agent。你不需要通过普通文本告诉模型“你可以用工具”,而是把工具定义为函数Schema,让模型的Function Calling能力直接把调用意图结构化输出,理解准确率和响应速度都比老式文本ReAct好很多。temperature=0.8:数字分身要有“人味”,就不能把temperature设太低。设成0会让每次回答都四平八稳,没有情绪起伏。0.8是我测试下来比较自然的数值,既有一定创造性,又不会跑偏。max_tokens=1024:回答长度上限。分身的回答应该控制在口语化对话范围,太长了不像聊天,像在写论文。max_iterations=5:Agent单轮最多循环5次(思考+调用工具+观察)。设太低,遇到需要二次搜索的问题会中断;设太高,模型会在某些情况下来回搜索原地打转。handle_parsing_errors=True:加入这一行是因为它会自动处理模型输出格式异常导致的解析错误,避免整个Agent直接崩掉。
调用方式也是标准写法:
def chat_with_digital_human(user_input): response = agent_executor.invoke({"input": user_input}) return response["output"]想查看每一步模型内部判断过程时,把verbose=True打开。控制台会打印完整链路:模型想到了什么、调用了什么工具、拿到了什么结果。这个输出对理解Agent行为、排查问题极其重要。
6. 实测效果与坑位记录:从“幻觉严重”到“有问必应”
把一个Demo从跑通到好用,中间隔着一堆细节。我把实测过程中踩到的几个典型问题和调整方法列出来,这些都是单看Langchain官方文档很难学到的。
6.1 搜索引擎结果与角色语气冲突
最开始的版本里,模型在搜索后直接念搜索结果:标题、日期、来源网站名,十分生硬。一打开对话就觉得是在用“搜索AI”,完全不像一个真人在说话。
我加了一行规则到Prompt:“搜索结果只能作为参考信息,必须在吸收信息后用人物自己的口吻重新组织语言,禁止直接朗读原文。”加上这条后,回答自然了很多。它的本质是一种约束模型“把资料转化为观点”的方式,这也是数字分身和普通搜索问答系统最核心的区别。
6.2 多轮对话中的“人设漂移”
另一个常见问题:对话进行到第三四轮之后,模型开始忘记自己的人设,回答变得异常正经,动不动就说“作为一个人工智能”。这是因为上下文中新话题挤占了原来System Prompt的注意力。
我用两个方法解决了这个问题:
第一,在每一轮用户消息前做一次人设提醒。不需要重复完整人设,只要简单加一句:“请记住,你是某某,请以该人物的口吻回答问题。”这样做能把人设权重重新拉高。
第二,缩短记忆窗口。把k从10降到6。一开始还担心降低记忆窗口会让分身“失忆”,实测发现6轮足够应对大多数对话,而且上下文缩短后模型做人设保持的效果明显好于长记忆。长记忆带来的偏移风险,比短记忆带来的遗忘成本还高。
6.3 Function Calling模式下工具调用的失败重试
OpenAI的Function Calling在大多数情况下调用都很准,但偶尔会出现一个问题:模型生成了工具调用请求,但没有按预期执行;或者请求参数格式正确,但Langchain在解析时抛出JSONDecodeError。这类问题我用了几种手段兜底:
- 给工具传入的
func里加上异常捕获,一旦搜索失败就返回明确的错误提示; - 设置
handle_parsing_errors=True,让Agent在解析失败时自动把原始输出内容反馈给模型,让模型自行纠正; - 使用
verbose=True观察失败时的模型原始输出,及时调整Prompt措辞。
6.4 对耗时敏感场景的优化思路
百度搜索API平均响应大概在0.5到1秒,加上模型推理的1到2秒,整体响应时间大概2到3秒。对于聊天场景已经可接受。但如果你希望进一步降低延迟,可以尝试两个方向:
第一,在搜索环节用“关键词精简”策略。用户的提问往往很长,直接拿全句去搜索,效果反而不如提取核心名词去搜。最简单的做法,是在调用搜索工具前,先让模型把用户问题转成一个5到10个字的关键词查询串。这一步能把搜索命中率和响应时间都优化不少。
第二,对搜索结果做预清洗。百度搜索API返回的结果一般含标题、摘要、链接,还有一些导航类噪声。直接在工具函数里截断summary字段,只保留前150个字符,能明显减少后续模型处理这些文本所需消耗的Token数,响应速度也更快。
7. 后续扩展方向与我的个人经验总结
第一篇的核心任务到这里就算完成了:Langchain框架搭建好了一个Agent,百度搜索API提供了实时信息输入,Prompt工程赋予了这个数字分身一个可信的角色人格。把这三个环节真跑起来后我第一次感受到,所谓的“数字分身”本质上是一种操作系统级的身份模拟,不再是单纯的提示词游戏。
如果你想把项目往深做,我建议按以下顺序迭代:
第一,做专属知识库。把目标人物的书籍、访谈、文章全部清洗成向量库,在Agent里加一个检索步骤。这比往Prompt里塞文本有效得多,也更隐性。Langchain维护了丰富的VectorStore集成,结合Embedding模型大概半天就能搭好。
第二,增加实时语音。用流式语音识别把用户语音转成文字,大模型生成回答后再用语音合成播报出去,整个交互体验会立刻上一个台阶。这篇里讲的Agent骨架完全不用变,只需要在输入输出两端接两个语音模块。
第三,接入长短期记忆。用向量数据库存储每轮对话的关键信息,让分身在下次见面时还能记住“昨天聊过什么”。这块Langchain提供了一些长期记忆抽象层,用起来体验不错。
最后分享一个我在调这个项目时的个人体会:做数字分身这类项目,最容易犯的错是过度追求“像”,比如花大量精力去堆人设细节,却在工具调用、知识边界这些地基性问题上偷懒。结果就是分身刚上线头几句像模像样,一旦用户问点新鲜的、实时的问题,马上就露馅。正确的路线一定是先把“工具与知识”的地基打牢,再回去润色语言风格和情绪表达,项目才能越做越有意思。