AI间对话APK制成
本文记录了如何使用API-KEY来实现“AI间相互对话”的功能。
笔者听闻优秀的从创作者而言,当角色设定丰满时,只需要思考场景,角色就会自己“动起来”。基于这样的想法,做出了一个AI与AI间对话的应用。技术上没有什么太大含量,更多的是作为实验性质的AI应用的产物。
设计如下:
一、类设计
Character类
角色类。每个角色维护自己的对话历史记录,并支持与其他角色类交互。
类属性:
name:角色名称。
prompt:角色的第一个提示词(以system角色输入的Prompt)。
initial:角色的初始发言(可选,同一个对话中只能又1个“初始发言”的角色)。
history:对话历史记录,格式为字典列表,包含以下字段:
role:发言者角色(system/user(其他Character扮演)/assistant)。
content:发言内容。
current_seq:发言的序列编号,用于后续手动修改内容或截取。
last_output:角色最近一次的输出内容,用这个作为其他Character类的输入。
used_tokens:累计使用的token数量(用于成本计算)。
类方法:
build_ipt_history:将history中的内容转化为OpenAI包可识别的格式。
chat_to:与其他角色对话,接收输入并生成回复,支持搜索、流式输出等参数;并更新自身历史与used_tokens。
class Character:def __init__(self,name:str,prompt:str,initial:str=""):self.prompt = promptself.name = nameself.initial = initialself.history = [{"role":"system","content":self.prompt,"current_seq":-1}]self.current_seq = -1if len(initial)>0:self.current_seq += 1self.history.append({"role":"assistant","content":initial,"current_seq":self.current_seq})self.last_output = initialself.used_tokens = 0def build_ipt_history(self):res = []for i in self.history:res.append({"role":i["role"],"content":i["content"]})return resdef chat_to(self,s:str,enable_search:bool=False,stream:bool=False,temperature:float=0.3):self.history.append({"role":"user","content":s,"current_seq":self.current_seq})self.current_seq += 1extra_body = {}stream_kwargs = {}if enable_search:extra_body["enable_search"]=Trueif stream:stream_kwargs["stream"]=Truestream_kwargs["stream_options"]={"include_usage": True}else:extra_body["enable_thinking"]=Falsestream_kwargs["extra_body"]=extra_bodycompletion = CLIENT.chat.completions.create(model=MODEL,messages=self.build_ipt_history(),temperature=temperature,**stream_kwargs)if stream:res = ''for chunk in completion:try:tmp = json.loads(chunk.model_dump_json())res+=tmp["choices"][0]['delta']['content']except Exception as e:continuetry:self.used_tokens += json.loads(chunk.model_dump_json())["usage"]["total_tokens"]except Exception as e:print(e)self.used_tokens = 0else:res = completion.choices[0].message.contenttry:self.used_tokens += completion.usage.prompt_tokens+completion.usage.completion_tokensexcept Exception as e:print(e)self.used_tokens = 0self.last_output = resself.history.append({"role":"assistant","content":res,"current_seq":self.current_seq})return res
ChatBetweenAI类
协调类。管理并维护多个Character实例(现只支持2个Character)之间的对话流程。
类属性:
npcs:存储所有角色的字典(键为角色名,值为 Character 实例)。
initial_npc:初始发言者(根据Character中的initial参数确定)。
history:全局对话历史记录。
change_log:对话变更日志(用于版本控制或回溯)。
TOKEN_FOR_SPLIT_DIFFERENT_NPC/TOKEN_FOR_SPLIT_NPC_AND_CONTENT/TOKEN_FOR_SPLIT_NPC_BEFORE_CHANGE/TOKEN_FOR_SPLIT_CHANGE_BEFORE_AND_AFTER:用于分割不同对话部分的特殊标记符。
seq_history_length_dict:记录每个角色的对话序列长度,主要方便截取
类方法:
add_system_prompt:为某个npc添加系统提示词
argue:多轮对话主控方法,指定轮次数和对话参数
change_history:修改特定位置的对话历史
change_log_2_txt:将手动修改的历史转化为文本格式用以导出
export:导出当前对话、NPC以及修改历史信息
export_change_log:导出手动修改的历史
export_dialogue_to_csv:导出当前对话到csv中
export_history:导出当前对话历史
export_npc:导出npc信息
get_used_token:查看当前消耗的token数量
history_to_txt:将对话历史导出为文本文件
import_:导入历史对话、NPC以及修改历史
import_change_log:导入手动修改历史
import_history:导入历史对话
import_npc:导入NPC信息
oneRoundArgueBetweenTwoAI:处理两个角色的单轮对话。
truncate:截断历史记录
txt_2_change_log:将保存的文本“恢复”为修改记录
txt_to_history:将保存的文本“恢复”为历史记录
class ChatBetweenAI:def __init__(self,*args):self.npcs = {}self.initial_npc = None #注意:initial_npc是先“发话”的那个人,他初始化时不应该有initial参数for i in args:if type(i)==Character:self.npcs[i.name] = iif len(i.initial)>0:self.initial_npc = i.nameself.history = []self.change_log = []self.TOKEN_FOR_SPLIT_DIFFERENT_NPC ="<split_for_different_npc>"self.TOKEN_FOR_SPLIT_NPC_AND_CONTENT = "<split_for_npc_and_content>"self.TOKEN_FOR_SPLIT_NPC_BEFORE_CHANGE = "<split_for_npc_and_begore_change>"self.TOKEN_FOR_SPLIT_CHANGE_BEFORE_AND_AFTER = "<split_for_change_before_and_after>"self.seq_history_length_dict = {}def get_used_token(self):return sum([i.used_tokens for i in self.npcs.values()])def argue(self,rnd=5,enable_search:bool=False,stream:bool=False,temperature:float=0.3):"""两个AI交流暂时只支持2个NPC"""if len(self.npcs.keys())==2: # first:第一个说话的人, second:第二个说话的人,第一个传入AI进行推理的人first = self.npcs[self.initial_npc]second_name = [i for i in list(self.npcs.keys()) if i != self.initial_npc][0]second = self.npcs[second_name]# 第一轮,first不用传入任何对话if len(self.history)==0:self.history.append({"role":self.initial_npc,"content":first.initial,"role_seq":first.current_seq})res = second.chat_to(first.initial)self.history.extend([{"role":second.name,"content":res,"role_seq":second.current_seq}])self.seq_history_length_dict[0]=len(self.history)rnd -= 1for i in range(rnd):self.oneRoundArgueBetweenTwoAI(first,second,enable_search,stream,temperature)def oneRoundArgueBetweenTwoAI(self,first:Character,second:Character,enable_search:bool=False,stream:bool=False,temperature:float=0.3):first_ipt = second.last_outputfirst_output = first.chat_to(first_ipt,enable_search,stream,temperature)second_output = second.chat_to(first_output,enable_search,stream,temperature)self.history.extend([{"role":first.name,"content":first_output,"role_seq":first.current_seq},{"role":second.name,"content":second_output,"role_seq":second.current_seq}])self.seq_history_length_dict[second.current_seq]=len(self.history)def change_history(self,idx:int,s:str):if self.history[idx]["role"] == "system":assert "不能修改历史prompt!"origin = self.history[idx]["content"]self.history[idx]["content"] = sorigin_role = self.history[idx]["role"]self.change_log.append({"role":origin_role,"origin":origin,"updated":s,"role_seq":self.history[idx]["role_seq"]})# 修改Character对象中地历史role_seq = self.history[idx]["role_seq"]npc_idx = [i for i,j in enumerate(self.npcs[origin_role].history) if j["role"]=="assistant"][role_seq]self.npcs[origin_role].history[npc_idx]["content"] = sif len(self.npcs[origin_role].history) == npc_idx+1:self.npcs[origin_role].last_output = s# 非initial的npc需要改“user”if self.initial_npc == origin_role:for npc in self.npcs.keys():if npc!=self.initial_npc:ipt_idx = [i for i,j in enumerate(self.npcs[npc].history) if j["role"]=="user"][role_seq]self.npcs[npc].history[ipt_idx]["content"] = sdef truncate(self,last_seq:int):"""暂时只支持2个NPC最后一个部分为“initial_npc”的发言顺序:initial_npc(current_seq=0)-> second(current_seq=0)->(一轮开始)first(current_seq=1)->second(current_seq=1)(一轮结束)"""if self.npcs[self.initial_npc].current_seq<=last_seq:returnself.history = self.history[0:self.seq_history_length_dict[last_seq]]for _,i in self.npcs.items():i.history = [n for n in i.history if n["current_seq"]<=last_seq and not (n["current_seq"]==last_seq and n["role"]!="assistant")]i.last_output = [n for n in i.history if n["role"]=="assistant"][-1]["content"]i.current_seq = last_seqdef add_system_prompt(self,prompt_dict:dict):for k,v in prompt_dict.items():if k not in self.npcs.keys():assert f"{k}不存在!"for k,v in prompt_dict.items():self.npcs[k].history.append({"role":"system","content":v,"current_seq":self.npcs[k].current_seq})def history_to_txt(self,history):# import和export还需加入current_seqcontent = self.TOKEN_FOR_SPLIT_DIFFERENT_NPC.join([self.TOKEN_FOR_SPLIT_NPC_AND_CONTENT.join([str(i) for i in i.values()]) for i in history]) # 我不能确定AI输出的内容是否会有特殊字符,故而使用特殊字符进行分割return contentdef change_log_2_txt(self):content = []for k,d in enumerate(self.change_log):role = d["role"]before = d["origin"]after = d["updated"]role_seq = d["role_seq"]content.append(role+self.TOKEN_FOR_SPLIT_NPC_BEFORE_CHANGE+before+self.TOKEN_FOR_SPLIT_CHANGE_BEFORE_AND_AFTER+after+self.TOKEN_FOR_SPLIT_CHANGE_BEFORE_AND_AFTER+str(role_seq)) # 我不能确定AI输出的内容是否会有特殊字符,故而使用特殊字符进行分割return self.TOKEN_FOR_SPLIT_DIFFERENT_NPC.join(content)def txt_2_change_log(self,s:str):if len(s)==0:return []content = s.split(self.TOKEN_FOR_SPLIT_DIFFERENT_NPC)res = []for i in content:tmp = i.split(self.TOKEN_FOR_SPLIT_NPC_BEFORE_CHANGE)role = tmp[0]chage_before_and_after = tmp[1].split(self.TOKEN_FOR_SPLIT_CHANGE_BEFORE_AND_AFTER)change_before = chage_before_and_after[0]change_after = chage_before_and_after[1]role_seq = chage_before_and_after[2]res.append({"role":role,"origin":change_before,"updated":change_after,"role_seq":role_seq})return resdef export_history(self,name:str=""):export_file_name = "_".join([i for i in self.npcs.keys()])if len(name) > 0:export_file_name = nameelse:export_file_name += datetime.now().strftime("%Y%m%d%H%M%S")content = self.history_to_txt(self.history)with open(export_file_name+".txt","w",encoding="utf-8") as f:f.write(content)def export_npc(self,name:str=""):export_file_name = "_".join([i for i in self.npcs.keys()])if len(name) > 0:export_file_name = nameelse:export_file_name += datetime.now().strftime("%Y%m%d%H%M%S")res_data = pd.DataFrame(columns=["prompt","name","initial","history","last_output"])for _,v in self.npcs.items():tmp = pd.DataFrame(pd.Series({"prompt":v.prompt,"name":v.name,"initial":v.initial,"history":self.history_to_txt(v.history),"last_output":v.last_output})).Tres_data = pd.concat([res_data,tmp]).reset_index(drop=True)res_data.to_csv(export_file_name+".csv",index=None,encoding="utf-8")def export_change_log(self,name:str=""):export_file_name = "_".join([i for i in self.npcs.keys()])if len(name) > 0:export_file_name = nameelse:export_file_name += datetime.now().strftime("%Y%m%d%H%M%S")content = self.change_log_2_txt()with open(export_file_name+".txt","w",encoding="utf-8") as f:f.write(content)def export(self,filename:str):self.export_npc(filename+"_NPC")self.export_history(filename+"_history")self.export_change_log(filename+"_change_log")def txt_to_history(self,s:str,tpe:str = "Chat"):if len(s)==0:return []res = s.split(self.TOKEN_FOR_SPLIT_DIFFERENT_NPC)res_list = []for i in res:tmp = i.split(self.TOKEN_FOR_SPLIT_NPC_AND_CONTENT)if tpe == "Chat":res_list.append({"role":tmp[0],"content":tmp[1],"role_seq":int(tmp[2])})else:res_list.append({"role":tmp[0],"content":tmp[1],"current_seq":int(tmp[2])})return res_listdef import_history(self,filename:str):with open(filename,encoding="utf-8") as f:s = f.read()# history备份#self.export_history()self.history = []try:self.history=self.txt_to_history(s)for i,x in enumerate(self.history):self.seq_history_length_dict[x["role_seq"]]=i+1except Exception as e:assert f"格式错误:{e}"def import_npc(self,filename:str):# npc备份#self.export_npc()npc_df = pd.read_csv(filename,encoding="utf-8")npc_df = npc_df.fillna("")for i in npc_df.index:npc_para = dict(npc_df.iloc[i,:])self.npcs[npc_para["name"]] = Character(name=npc_para["name"],prompt=npc_para["prompt"],initial=npc_para["initial"])self.npcs[npc_para["name"]].last_output = npc_para["last_output"]self.npcs[npc_para["name"]].history = self.txt_to_history(npc_para["history"],tpe="NPC")self.npcs[npc_para["name"]].current_seq = max([i["current_seq"] for i in self.npcs[npc_para["name"]].history if i["role"]=="assistant"])if len(npc_para["initial"])>0:self.initial_npc = npc_para["name"]def import_change_log(self,filename:str):#self.export_change_log()with open(filename,encoding="utf-8") as f:data = f.read()self.change_log = self.txt_2_change_log(data)def import_(self,filename:str):NPC_file = filename+"_NPC.csv"history_file = filename+"_history.txt"changelog_file = filename+"_change_log.txt"if not (os.path.exists(NPC_file) and os.path.exists(history_file) and os.path.exists(changelog_file)):assert "缺失存档,请检查存档名称!"self.import_npc(NPC_file)self.import_change_log(changelog_file)self.import_history(history_file)def export_dialogue_to_csv(self,file_name:str):step = len(self.npcs.keys())res = pd.DataFrame(columns=list(self.npcs.keys()))idx = 0while idx<len(self.history):this_round = self.history[idx:idx+step]tmp = {k:None for k in self.npcs.keys()}for i in this_round:tmp[i["role"]]=i["content"]res = pd.concat([res,pd.DataFrame(pd.Series(tmp)).T],axis=0)idx+=stepres.to_csv(file_name+".csv",index=False,encoding="utf-8")
二、示例代码
promptA = history_prompt+"\n你是一个经验丰富且技术高超的辩论赛辩手,上面是某场辩论赛的正方/反方的一辩与二辩的论点。现在请以正方辩手的身份加入自由辩论环节,回答由用户扮演的反方辩手的质疑,同时需要注意回答完成之后对反方的观点提出新的质疑,让用户回答,最好能够有发散性思维,从之前没有提到的角度质疑用户从而形成头脑风暴。限制50字以内,只要说的语言即可,不要加入动作或神态等其它描写。"
promptB = history_prompt+"\n你是一个经验丰富且技术高超的辩论赛辩手,上面是某场辩论赛的正方/反方的一辩与二辩的论点。现在请以反方辩手的身份加入自由辩论环节,回答由用户扮演的正方辩手的质疑,同时需要注意回答完成之后对正方的观点提出新的质疑,让用户回答,最好能够有发散性思维,从之前没有提到的角度质疑用户从而形成头脑风暴。限制50字以内,只要说的语言即可,不要加入动作或神态等其它描写。你的第一句话是:请问对方辩友,如何解释魏尔斯特拉斯的ε-δ语言从未经过通俗转化,却成为现代分析学基石?这是否证明专业文章的价值穿透力无需\"转\"的中介?"
A = Character(name="正方",prompt=promptA,initial="")
B = Character(name="反方",prompt=promptB,initial='请问对方辩友,如何解释魏尔斯特拉斯的ε-δ语言从未经过通俗转化,却成为现代分析学基石?这是否证明专业文章的价值穿透力无需"转"的中介?')
Chat = ChatBetweenAI(A,B)
Chat.argue(rnd=5,enable_search=True,stream=True)
完整的辩论可以参照我的B站视频
三、安卓APK制成
Java和Kotlin似乎没有OpenAI包,所以需要使用阿里云百炼官方提供的Dashscope。
Character类:
data class Msg(val role: String,val content: String,@SerializedName("current_seq")val currentSeq: Int
)class Character(val name: String,val prompt: String,val initial: String = ""
) {public var history = mutableListOf<Msg>().apply {add(Msg("system", prompt, -1))}var currentSeq = -1public setvar lastOutput = initialpublic setvar usedTokens = 0public setinit {if (initial.isNotEmpty()) {currentSeq += 1history.add(Msg("assistant", initial, currentSeq))lastOutput = initial}}fun addMessage(role: String, content: String) {if (role == "assistant") {currentSeq += 1lastOutput = content}history.add(Msg(role, content, currentSeq))}fun buildIptHistory(): List<Map<String, String>> {return history.map {mapOf("role" to it.role, "content" to it.content)}}fun getFullHistory(): List<Msg> = history.toList()fun reset() {history.clear()history.add(Msg("system", prompt, -1))currentSeq = -1lastOutput = ""usedTokens = 0if (initial.isNotEmpty()) {addMessage("assistant", initial)}}fun getLastMessage(): Msg? = history.lastOrNull()fun chatTo(key:String,modelName:String,s:String,enable_Thinking:Boolean,enable_search:Boolean,stream:Boolean,temprature:Float):String{addMessage("user",s)//currentSeq+=1val iptMsg = createMessageArray()val params = GenerationParam.builder().apiKey(key).model(modelName).enableSearch(enable_search).messages(iptMsg).resultFormat(GenerationParam.ResultFormat.MESSAGE).incrementalOutput(stream).enableThinking(enable_Thinking).temperature(temprature).build()val gen = Generation();if(stream){val GenResult = gen.streamCall(params)var res = ""GenResult.blockingForEach { i-> res+=parse_json_to_history(JsonUtils.toJson(i))}addMessage("assistant",res)return res}val GenResult = gen.call(params)val res = parse_json_to_history(JsonUtils.toJson(GenResult))addMessage("assistant",res)return res}fun createMessageArray():List<Message>{val res = ArrayList<Message>()val roleDict = mapOf("assistant" to Role.ASSISTANT,"system" to Role.SYSTEM,"user" to Role.USER);for (msg in history){val current_Role:Role = roleDict[msg.role] ?: Role.USER;res.add(createMessage(current_Role,msg.content));}return res}fun parse_json_to_history(jsonStr:String):String{val root = JsonParser.parseString(jsonStr).getAsJsonObject();val contents = root.getAsJsonObject("output").getAsJsonArray("choices").map{it.asJsonObject.getAsJsonObject("message").get("content").asString}.joinToString ("")try{val totalTokens = root.getAsJsonObject("usage").get("total_tokens").getAsInt()usedTokens += totalTokens}catch(e: Exception){usedTokens = 0}return contents}fun createMessage(role:Role,content:String):Message{return Message.builder().role(role.getValue()).content(content).build();}fun truncateHistory(lastSeq: Int){history = history.filter { n ->(n.currentSeq as Int <= lastSeq) &&!(n.currentSeq as Int == lastSeq && n.role != "assistant")}.toMutableList()lastOutput = history.last { it.role == "assistant" }.contentcurrentSeq = lastSeq}fun addSystemPrompt(prompt: String){if(history.lastOrNull()?.role.equals("system")){history[history.size-1] = Msg("system", prompt, currentSeq)}else{history.add(Msg("system", prompt, currentSeq))}}fun importHistory(historyList: List<Map<String, Any>>) {history.clear()historyList.forEach { entry ->history.add(Msg(role = entry["role"] as String,content = entry["content"] as String,currentSeq = (entry["current_seq"] as? Int) ?: 0))}// 更新当前状态lastOutput = history.lastOrNull { it.role == "assistant" }?.content ?: ""currentSeq = history.maxOfOrNull { it.currentSeq } ?: -1}
}
ChatBetweenAI类:
class ChatBetweenAI {val npcs = mutableMapOf<String, Character>()var initialNpc: String? = nullval history = mutableListOf<Map<String, Any?>>()val changeLog = mutableListOf<Map<String, Any?>>()val seqHistoryLengthDict = mutableMapOf<Int, Int>()companion object {const val TOKEN_FOR_SPLIT_DIFFERENT_NPC = "<split_for_different_npc>"const val TOKEN_FOR_SPLIT_NPC_AND_CONTENT = "<split_for_npc_and_content>"const val TOKEN_FOR_SPLIT_NPC_BEFORE_CHANGE = "<split_for_npc_and_before_change>"const val TOKEN_FOR_SPLIT_CHANGE_BEFORE_AND_AFTER = "<split_for_change_before_and_after>"const val DELIMITER_COLUMN_FOR_CSV = "<DELIMITER_COLUMN_FOR_CSV>"const val DELIMITER_ROW_FOR_CSV = "<DELIMITER_ROW_FOR_CSV>"}fun getUsedToken(): Int {return npcs.values.sumOf { it.usedTokens }}suspend fun argue(rnd: Int = 1,key:String,modelName:String,enableThinking:Boolean,enableSearch: Boolean, stream: Boolean, temperature: Float):Flow<Int> = flow{var status=0if (npcs.size != 2) {throw IllegalArgumentException("目前只支持 2 个 NPC 的交流")}var rnd = rndval first = npcs[initialNpc] ?: throw IllegalStateException("初始 NPC 未设置")val secondName = npcs.keys.first { it != initialNpc }val second = npcs[secondName] ?: throw IllegalStateException("找不到第二个 NPC")// 第一轮对话if (history.filter { n->n.get("role")!="system" }.isEmpty()) {history.add(mapOf("role" to initialNpc,"content" to first.initial,"role_seq" to first.currentSeq))val res = second.chatTo(key,modelName,first.initial, enableThinking, enableSearch, stream, temperature)history.add(mapOf("role" to second.name,"content" to res,"role_seq" to second.currentSeq))rnd -= 1seqHistoryLengthDict[0] = history.sizeemit(status)status+=1if (rnd < 1){emit(-999)return@flow}}// 后续轮次for (i in 1 .. rnd) {oneRoundArgueBetweenTwoAI(key,modelName,first, second,enableThinking, enableSearch, stream, temperature)emit(status)status+=1}}private suspend fun oneRoundArgueBetweenTwoAI(key:String,modelName:String,first: Character,second: Character,enableThinking: Boolean,enableSearch: Boolean,stream: Boolean,temperature: Float) {val firstInput = second.lastOutputval firstOutput = first.chatTo(key,modelName,firstInput, enableThinking, enableSearch, stream, temperature)val secondOutput = second.chatTo(key,modelName,firstOutput, enableThinking, enableSearch, stream, temperature)history.add(mapOf("role" to first.name,"content" to firstOutput,"role_seq" to first.currentSeq))history.add(mapOf("role" to second.name,"content" to secondOutput,"role_seq" to second.currentSeq))seqHistoryLengthDict[second.currentSeq] = history.size}// fun changeHistory(idx: Int, s: String) {
// if (history[idx]["role"] == "system") {
// throw IllegalStateException("不能修改历史 prompt!")
// }
//
// val origin = history[idx]["content"] as String
// val originRole = history[idx]["role"] as String
// val roleSeq = history[idx]["role_seq"] as Int
//
// // 更新历史记录
// val newEntry = history[idx].toMutableMap().apply { put("content", s) }
// history[idx] = newEntry
//
// // 添加到变更日志
// changeLog.add(mapOf(
// "role" to originRole,
// "origin" to origin,
// "updated" to s,
// "role_seq" to roleSeq
// ))
//
// // 更新对应 NPC 的历史
// val npc = npcs[originRole] ?: return
// val npcHistory = npc.getFullHistory()
//
// // 找到对应的历史记录并更新
// val npcIdx = npcHistory.indexOfFirst {
// it.role == "assistant" && it.currentSeq == roleSeq
// }
//
// if (npcIdx != -1) {
// npc.updateMessageContent(npcIdx, s)
//
// // 如果是最后一条消息,更新 lastOutput
// if (npcHistory.lastIndex == npcIdx) {
// npc.lastOutput = s
// }
// }
//
// // 更新其他 NPC 的输入
// if (initialNpc == originRole) {
// npcs.values.forEach { otherNpc ->
// if (otherNpc.name != originRole) {
// val otherHistory = otherNpc.getFullHistory()
// val inputIdx = otherHistory.indexOfFirst {
// it.role == "user" && it.currentSeq == roleSeq
// }
//
// if (inputIdx != -1) {
// otherNpc.updateMessageContent(inputIdx, s)
// }
// }
// }
// }
// }fun truncate(lastSeq: Int) {if ((npcs[initialNpc]?.currentSeq ?: 0) <= lastSeq) returnval truncateIndex = seqHistoryLengthDict[lastSeq] ?: returnhistory.subList(truncateIndex, history.size).clear()npcs.values.forEach { npc ->npc.truncateHistory(lastSeq)}}fun addSystemPrompt(promptDict: Map<String, String>) {promptDict.forEach { (name, prompt) ->if (!npcs.containsKey(name)) {throw IllegalArgumentException("$name 不存在!")}}promptDict.forEach { (name, prompt) ->npcs[name]?.addSystemPrompt(prompt)history.add(mapOf("role" to "system","content" to prompt,"role_seq" to npcs[name]?.currentSeq))}}fun historyToTxt(historyList: List<Map<String, Any?>>): String {return historyList.joinToString(TOKEN_FOR_SPLIT_DIFFERENT_NPC) { entry ->listOf(entry["role"] as String,entry["content"] as String,entry["role_seq"].toString()).joinToString(TOKEN_FOR_SPLIT_NPC_AND_CONTENT)}}fun changeLogToTxt(): String {return changeLog.joinToString(TOKEN_FOR_SPLIT_DIFFERENT_NPC) { log ->listOf(log["role"] as String,log["origin"] as String,log["updated"] as String,log["role_seq"].toString()).joinToString(TOKEN_FOR_SPLIT_CHANGE_BEFORE_AND_AFTER)}}private fun generateFileName(): String {return npcs.keys.joinToString("_") + SimpleDateFormat("yyyyMMddHHmmss",Locale.getDefault()).format(Date())}fun exportHistory(context: Context,name: String = "") {val fileName = if (name.isNotEmpty()) name else generateFileName()val dir = context.getFilesDir().toString()+"$fileName.csv"File(dir).writeText(historyToTxt(history))}fun exportNpc(context: Context,name: String = "") {val fileName = if (name.isNotEmpty()) name else generateFileName()val csvContent = buildString {npcs.values.forEach { char ->val row = listOf(char.prompt,char.name,char.initial,historyToTxt(char.getFullHistory().map {mapOf("role" to it.role.toString(),"content" to it.content.toString(),"role_seq" to it.currentSeq.toString())}),char.lastOutput).joinToString ( DELIMITER_COLUMN_FOR_CSV ) + DELIMITER_ROW_FOR_CSVappend(row)}}val dir = context.getFilesDir().toString()+"$fileName.csv"File(dir).writeText(csvContent)}fun exportChangeLog(context: Context,name: String = "") {val fileName = if (name.isNotEmpty()) name else generateFileName()val dir = context.getFilesDir().toString()+"$fileName.csv"File(dir).writeText(changeLogToTxt())}fun export(context: Context,baseName: String) {exportNpc(context,"${baseName}_NPC")exportHistory(context,"${baseName}_history")exportChangeLog(context,"${baseName}_change_log")}fun txtToHistory(s: String, type: String = "Chat"): List<Map<String, Any>> {if (s.isEmpty()) return emptyList()return s.split(TOKEN_FOR_SPLIT_DIFFERENT_NPC).map { entry ->val parts = entry.split(TOKEN_FOR_SPLIT_NPC_AND_CONTENT)when (type) {"Chat" -> mapOf("role" to parts[0],"content" to parts[1],"role_seq" to parts[2].toInt())else -> mapOf("role" to parts[0],"content" to parts[1],"current_seq" to parts[2].toInt())}}}fun importHistory(context: Context,filename: String) {val dir = context.getFilesDir().toString()+"$filename"val content = File(dir).readText()history.clear()history.addAll(txtToHistory(content))// 重建序列字典seqHistoryLengthDict.clear()history.forEachIndexed { index, entry ->(entry["role_seq"] as? Int)?.let { seq ->seqHistoryLengthDict[seq] = index + 1}}}fun importNpc(context: Context,filename: String) {val dir = context.getFilesDir().toString()+"$filename"val content = File(dir).readText().split(DELIMITER_ROW_FOR_CSV)if (content.size < 2) returnfor (line in content) {val values = line.split(DELIMITER_COLUMN_FOR_CSV)if (values.size < 5) continueval name = values[1]val prompt = values[0]val initial = values[2]val historyText = values[3]val lastOutput = values[4]val char = Character(name, prompt, initial)char.lastOutput = lastOutputchar.importHistory(txtToHistory(historyText, "NPC"))npcs[name] = charif (initial.isNotEmpty()) {initialNpc = name}}}fun importChangeLog(context: Context,filename: String) {val dir = context.getFilesDir().toString()+"$filename"val content = File(dir).readText()changeLog.clear()if (content.isEmpty()) returncontent.split(TOKEN_FOR_SPLIT_DIFFERENT_NPC).forEach { entry ->val parts = entry.split(TOKEN_FOR_SPLIT_CHANGE_BEFORE_AND_AFTER)if (parts.size >= 4) {changeLog.add(mapOf("role" to parts[0],"origin" to parts[1],"updated" to parts[2],"role_seq" to parts[3].toInt()))}}}fun import(context: Context,baseName: String) {importNpc(context,"${baseName}_NPC.csv")importHistory(context,"${baseName}_history.csv")importChangeLog(context,"${baseName}_change_log.csv")}fun exportDialogueToCsv(fileName: String) {val step = npcs.sizeif (step == 0) returnval csvContent = buildString {// 标题行appendLine(npcs.keys.joinToString(","))// 内容行var idx = 0while (idx < history.size) {val round = history.subList(idx, minOf(idx + step, history.size))val row = npcs.keys.map { npcName ->round.find { it["role"] == npcName }?.get("content")?.toString() ?: ""}appendLine(row.joinToString(","))idx += step}}File("$fileName.csv").writeText(csvContent)}}
完整的项目代码可以查看:我的github链接
四、未来展望
现在2个AI对话似乎没有过多意义,但是未来作为创作辅助或许有其价值;之后或许可以让2个角色通过MCP调用函数以实现更为逼真的交互,或者加入更多角色以实现多角色对话,从而进行创作辅助或进行实验。我觉得还是可能有一定价值的。