LLM大模型:大模型应用(五)模型记忆力代码实战,memory最佳实践

前言

首先讲memory是很宽泛的概念,通常理解一个大模型的应用=思考力+行动力+记忆力+设定。通常意义上所谓的memory就是记忆力。

我接触的memory代码,是langchain上看到的,它给了我很大启发,后来还在我们整个团队内部分享,间接促成了我们对于memory的专项化建设。

对记忆力的抽象

简单的将记忆力分三种

  • 短期的,明确的记忆。不管信息有用没有,短期的记忆都应该是清楚地。
  • 长期的,模糊的记忆,记住的信息,可能缺失,也可能不准。就像你记不住三年前你立的flag。
  • 长期的,明确的记忆。比如你的年龄,身高等。可能好几年都不会有人问你年龄,但你通常是能记住的。

针对上面的分析,我们对应如下trait:

pub trait Memory{
    //短期记忆<-------------
    //加载上下文
    async fn load_context(&self,user:&str, max: usize) -> anyhow::Result<Vec<ChatCompletionRequestMessage>>;
    //追加会话日志,可以在上下文中获取到
    async fn add_session_log(&self,user:&str,record: Vec<ChatCompletionRequestMessage>);

    //长期明确记忆<-------------
    //拉取用户标签
    async fn get_user_tag(&self,user:&str,tag:&str) -> anyhow::Result<String>;
    //给用户贴标签
    async fn set_user_tage(&self,user:&str,kvs:HashMap<String,String>);

    //长期模糊记忆<-------------
    //召回长期记忆
    async fn recall_summary(&self,user:&str,query: &str,n:usize)-> anyhow::Result<Vec<String>>;
    //将记忆进行总结
    async fn summary_history(&self,user:&str);
}

代码实现

短期记忆

通常我们把短期记忆放在上下文里面,存储的放在任意数据库中。

我这里实现很简单,直接放在了内存里作为一个列表存储,重启就没了。

长期-明确记忆

这种的,我们只需要按照标签存储,如姓名,年龄。

我这里用了一个本地的KV数据库,保证我们重启后,还能够记住这些信息。

async fn add_session_log(&self,mut record: Vec<ChatCompletionRequestMessage>) {
    //fixme 存才并发错误
    #[allow(invalid_reference_casting)]
    unsafe {
        let sc = &mut *(&self.short_context as *const Vec<ChatCompletionRequestMessage> as *mut Vec<ChatCompletionRequestMessage>);
        sc.append(&mut record);
    }
}

async fn get_user_tag(&self,uid:&str, tag: &str) -> anyhow::Result<String> {
    let result = UIB.get(format!("{}-{}",uid,tag).as_str());
    return result
}

长期-模糊记忆

虽然人类的长期记忆是模糊的,但是我们的数据库存了所有对话信息,不管过去多久,实际上我们都能明确的找到用户的会话记录。也就是说我们可以很清楚的记得长期记忆。

但是受限于大模型的tokens,我们不能把这些记忆都放进去,并且很多内容是无效的,这个时候就需要我们对历史进行总结,并且在这些总结中,找到相关的内容。

这里就需要两个基本功能,总结和召回:

  • 总结:总结历史会话我们还是用大模型进行总结,并且定期离线,或者在线发起总结。
  • 召回:我这里用阿里云的DashVector+embedding召回
async fn recall_summary(&self,uid:&str, query: &str, n: usize) -> anyhow::Result<Vec<String>> {
    //这里是topn召回
    self.summary.top_n(uid,query,n).await
}

async fn summary_history(&self,uid:&str){
    let prompt:ChatCompletionRequestMessage = ChatCompletionRequestSystemMessageArgs::default()
        .content("你是阅读理解专家。你的目标是,从过往的对话,提取关键信息,进行总结。总结后的内容必须言简意赅,简洁明了。最总通过`summery_tool`记录下来。")
        .build()
        .unwrap()
        .into();

    let mut context = vec![prompt];
    let mut chat_context = match self.load_context(1000).await{
        Ok(o) => o,
        Err(e) => {
            wd_log::log_field("error",e).error("summary history load context error");
            return;
        }
    };
    context.append(&mut chat_context);

    let summery_tool = ChatCompletionToolArgs::default()
        .r#type(ChatCompletionToolType::Function)
        .function(FunctionObjectArgs::default()
            .name("summery_tool")
            .description("记录总结内容")
            .parameters(json!({
                "type": "object",
                "properties": {
                    "list": {
                        "type": "array",
                        "items":{
                            "type":"string",
                            "description": "条目",
                        },
                        "description": "总结得到的列表",
                    },
                },
                "required": ["location"],
            }))
            .build().unwrap())
        .build().unwrap();


    let chat_req = CreateChatCompletionRequestArgs::default()
        .max_tokens(4096u16)
        .model("gpt-4")
        .messages(context)
        .tools([summery_tool])
        .build()
        .unwrap();

    let client = Client::new();
    let resp = client.chat().create(chat_req).await.unwrap();
    let mut summery_list = vec![];
    for i in resp.choices {
        if i.message.tool_calls.is_some(){
            for j in i.message.tool_calls.unwrap(){
                println!("总结的内容:{}",j.function.arguments);
                if let Ok(sl) = serde_json::from_str::<SummeryList>(j.function.arguments.as_str()) {
                    summery_list = sl.list;
                    break
                }
            }
        }else{
            wd_log::log_field("role",i.message.role)
                .field("message",format!("{:?}",i))
                .info("summery not is tool");
        }
    }
    if let Err(e) = self.summary.insert(uid.to_string(),summery_list).await {
        wd_log::log_field("error",e).error("insert to dash vector error")
    }
}

工具实现

给用户贴标签

为了获取用户的长期准确记忆,我们不可能给用户一个问卷,让用户把所有信息都填一遍,所以,信息的收集应该来自对话。调起一个tool进行收集。所以我们需要实现一个function call。

pub struct UserTagsNode{
    inner:Arc<ShortLongMemoryMap>,
}
impl UserTagsNode {
    //注入到agent中,也就是对自身功能的描述
    pub fn as_openai_tool(&self) -> ChatCompletionTool {
        ChatCompletionToolArgs::default()
            .r#type(ChatCompletionToolType::Function)
            .function(FunctionObjectArgs::default()
                .name(self.id())
                .description("记录用户姓名,年龄")
                .parameters(Value::from_str(r#"{"type":"object","properties":{"tag":{"type":"string","description":"用户标签","enum":["name","age"]},"name":{"type":"string","description":"姓名"},"age":{"type":"integer","description":"年龄"}},"required":["tag"]}"#).unwrap())
                .build().unwrap())
            .build()
            .unwrap()
            .into()
    }
}

#[async_trait::async_trait]
impl Node for UserTagsNode {
    fn id(&self) -> String {
        "user_tag".into()
    }
    
    //按照前面博文的实现,我们需要给tool一个具体的节点实现
    async fn go(&self, ctx: Arc<Context>, mut args: TaskInput) -> anyhow::Result<TaskOutput> {
        let uid = user_id_from_ctx(ctx.as_ref());
        if let Some(s) = args.get_value::<ChatCompletionMessageToolCall>() {
            let input = s.function.arguments;
            let req = serde_json::from_str::<UserTagsNodeReq>(input.as_str())?;
            match req.tag.as_str() {
                "name"=> self.inner.set_user_tage(uid.as_str(),HashMap::from([("name".into(),req.name)])).await,
                "age"=> self.inner.set_user_tage(uid.as_str(),HashMap::from([("age".into(),req.age.to_string())])).await,
                _=> return anyhow::anyhow!("tag[{}] unknown",req.tag).err()
            };
            let msg: ChatCompletionRequestMessage = ChatCompletionRequestToolMessageArgs::default()
                .tool_call_id(s.id)
                .content("success")
                .build()
                .unwrap()
                .into();
            return go_next_or_over(ctx,msg)
        }else{
            anyhow::anyhow!("args not find").err()
        }
    }
}

主动总结

一般来说总结应该是离线的,但我们也应该允许用户主动总结,同样来一个tool支持。

pub struct SummeryNode{
    inner:Arc<ShortLongMemoryMap>,
}
impl SummeryNode {
    //对tool的描述
    pub fn as_openai_tool(&self) -> ChatCompletionTool {
        ChatCompletionToolArgs::default()
            .r#type(ChatCompletionToolType::Function)
            .function(FunctionObjectArgs::default()
                .name(self.id())
                .description("用户主动总结")
                .parameters(Value::from_str(r#"{"type":"object","properties":{"is":{"type":"boolean","description":"是否总结"}}}"#).unwrap())
                .build().unwrap())
            .build()
            .unwrap()
            .into()
    }
}

#[async_trait::async_trait]
impl Node for SummeryNode {
    fn id(&self) -> String {
        "summery".into()
    }
    //具体的执行节点
    async fn go(&self, ctx: Arc<Context>, mut args: TaskInput) -> anyhow::Result<TaskOutput> {
        let uid = user_id_from_ctx(ctx.as_ref());
        let input = args.get_value::<ChatCompletionMessageToolCall>().unwrap();
        self.inner.summary_history(uid.as_str()).await;
        let msg: ChatCompletionRequestMessage = ChatCompletionRequestToolMessageArgs::default()
            .tool_call_id(input.id)
            .content("success")
            .build()
            .unwrap()
            .into();
        go_next_or_over(ctx,msg)
    }
}

测试

将我们的memory,替换到上一篇博文中的single agent中,并且起一个窗口进行交互。具体代码我就不贴了。

效果

为了避免短期上下文对效果的影响,我们通过重启的方式丢弃掉短期上下文记忆。

  1. 首先起一个新用户,直接对话,如下图:
    • 这里可以看到prompt中关于用户和总结的信息都是空的
    • 我们在对话中告知了姓名和年龄,并且模型正确的记录了用户标签。

image.png

  1. 退出程序,重新启动,如下图:
    • 上面我们说的用户信息已经被记住,并正确组装到prompt中
    • 我们主动发起总结,记录总结信息

image.png

  1. 退出程序,重新启动,如下图:
    • 可以看到prompt中,总结被召回
    • 和模型对话,无论是年龄,还是过生日都能够正确回答。

image.png

尾语

上面代码中的总结召回还过于粗糙,实际应该在每轮对话中使用。我这里先实现memory的部分,完整版在prompt实现中贴出。

记忆力是大模型应用的最重要的事情之一,还有很多需要思考的东西,如果你有想法欢迎留言或评论。

在这里插入图片描述

如何学习AI大模型?

我在一线互联网企业工作十余年里,指导过不少同行后辈。帮助很多人得到了学习和成长。

我意识到有很多经验和知识值得分享给大家,也可以通过我们的能力和经验解答大家在人工智能学习中的很多困惑,所以在工作繁忙的情况下还是坚持各种整理和分享。但苦于知识传播途径有限,很多互联网行业朋友无法获得正确的资料得到学习提升,故此将并将重要的AI大模型资料包括AI大模型入门学习思维导图、精品AI大模型学习书籍手册、视频教程、实战学习等录播视频免费分享出来。

在这里插入图片描述

第一阶段: 从大模型系统设计入手,讲解大模型的主要方法;

第二阶段: 在通过大模型提示词工程从Prompts角度入手更好发挥模型的作用;

第三阶段: 大模型平台应用开发借助阿里云PAI平台构建电商领域虚拟试衣系统;

第四阶段: 大模型知识库应用开发以LangChain框架为例,构建物流行业咨询智能问答系统;

第五阶段: 大模型微调开发借助以大健康、新零售、新媒体领域构建适合当前领域大模型;

第六阶段: 以SD多模态大模型为主,搭建了文生图小程序案例;

第七阶段: 以大模型平台应用与开发为主,通过星火大模型,文心大模型等成熟大模型构建大模型行业应用。

在这里插入图片描述

👉学会后的收获:👈
• 基于大模型全栈工程实现(前端、后端、产品经理、设计、数据分析等),通过这门课可获得不同能力;

• 能够利用大模型解决相关实际项目需求: 大数据时代,越来越多的企业和机构需要处理海量数据,利用大模型技术可以更好地处理这些数据,提高数据分析和决策的准确性。因此,掌握大模型应用开发技能,可以让程序员更好地应对实际项目需求;

• 基于大模型和企业数据AI应用开发,实现大模型理论、掌握GPU算力、硬件、LangChain开发框架和项目实战技能, 学会Fine-tuning垂直训练大模型(数据准备、数据蒸馏、大模型部署)一站式掌握;

• 能够完成时下热门大模型垂直领域模型训练能力,提高程序员的编码能力: 大模型应用开发需要掌握机器学习算法、深度学习框架等技术,这些技术的掌握可以提高程序员的编码能力和分析能力,让程序员更加熟练地编写高质量的代码。

在这里插入图片描述

1.AI大模型学习路线图
2.100套AI大模型商业化落地方案
3.100集大模型视频教程
4.200本大模型PDF书籍
5.LLM面试题合集
6.AI产品经理资源合集

👉获取方式:
😝有需要的小伙伴,可以保存图片到wx扫描二v码免费领取【保证100%免费】🆓

在这里插入图片描述

  • 15
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值