本文记录了如何使用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 = prompt
self.name = name
self.initial = initial
self.history = [{
"role":"system",
"content":self.prompt,
"current_seq":-1
}]
self.current_seq = -1
if len(initial)>0:
self.current_seq += 1
self.history.append({"role":"assistant","content":initial,"current_seq":self.current_seq})
self.last_output = initial
self.used_tokens = 0
def build_ipt_history(self):
res = []
for i in self.history:
res.append({"role":i["role"],"content":i["content"]})
return res
def 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 += 1
extra_body = {}
stream_kwargs = {}
if enable_search:
extra_body["enable_search"]=True
if stream:
stream_kwargs["stream"]=True
stream_kwargs["stream_options"]={"include_usage": True}
else:
extra_body["enable_thinking"]=False
stream_kwargs["extra_body"]=extra_body
completion = 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:
continue
try:
self.used_tokens += json.loads(chunk.model_dump_json())["usage"]["total_tokens"]
except Exception as e:
print(e)
self.used_tokens = 0
else:
res = completion.choices[0].message.content
try:
self.used_tokens += completion.usage.prompt_tokens+completion.usage.completion_tokens
except Exception as e:
print(e)
self.used_tokens = 0
self.last_output = res
self.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] = i
if len(i.initial)>0:
self.initial_npc = i.name
self.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 -= 1
for 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_output
first_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"] = s
origin_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"] = s
if 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"] = s
def 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:
return
self.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_seq
def 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_seq
content = 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 content
def 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 res
def export_history(self,name:str=""):
export_file_name = "_".join([i for i in self.npcs.keys()])
if len(name) > 0:
export_file_name = name
else:
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 = name
else:
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})).T
res_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 = name
else:
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_list
def 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+1
except 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 = 0
while 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+=step
res.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 = -1
public set
var lastOutput = initial
public set
var usedTokens = 0
public set
init {
if (initial.isNotEmpty()) {
currentSeq += 1
history.add(Msg("assistant", initial, currentSeq))
lastOutput = initial
}
}
fun addMessage(role: String, content: String) {
if (role == "assistant") {
currentSeq += 1
lastOutput = 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 = -1
lastOutput = ""
usedTokens = 0
if (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+=1
val 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" }
.content
currentSeq = 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? = null
val 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=0
if (npcs.size != 2) {
throw IllegalArgumentException("目前只支持 2 个 NPC 的交流")
}
var rnd = rnd
val 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 -= 1
seqHistoryLengthDict[0] = history.size
emit(status)
status+=1
if (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.lastOutput
val 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) return
val truncateIndex = seqHistoryLengthDict[lastSeq] ?: return
history.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_CSV
append(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) return
for (line in content) {
val values = line.split(DELIMITER_COLUMN_FOR_CSV)
if (values.size < 5) continue
val 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 = lastOutput
char.importHistory(txtToHistory(historyText, "NPC"))
npcs[name] = char
if (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()) return
content.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.size
if (step == 0) return
val csvContent = buildString {
// 标题行
appendLine(npcs.keys.joinToString(","))
// 内容行
var idx = 0
while (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调用函数以实现更为逼真的交互,或者加入更多角色以实现多角色对话,从而进行创作辅助或进行实验。我觉得还是可能有一定价值的。