LLM大模型学习:大模型应用(四)基于gpt的代码实战:从零构建一个智能大脑,让多个智能体协同工作

前言

上篇博文中,说了copilot和agent的原理,这篇博文重点是代码实现。

首先看一下代码架构

image.png

  • runtime:能够按照一定策略,将一个复杂的任务按照,一个一个节点的方式运行起来。可以理解为对流程图的高度抽象。
  • Node:对节点抽象出的接口,上面所有黄色的节点,都要实现这个接口,并最终放在runtime上执行。
  • llm:基座大模型,tool:工具,script engine:脚本引擎,control node:节点控制器
  • single agent:按照论文的说法,是一个在某个场景下的专家,比如画画专家,购物专家。
  • multi agent:让多个专家协同工作
  • task flow:执行一个特定的任务流,相当于按照流程图执行任务。

本文重点:基于runtime实现single agentmulti agenttask flow我们以后再说。其实有了runtime,taskflow自动就实现了。

Runtime&Node

runtime的作用:假设我们有了一些能够运行的节点,当一个任务到达时,runtime需要按照一定的规则和流程,执行一系列节点,并最终给出一个结果。这里有两个需要注意的地方:

  • 每个节点都必须是对等
  • 节点之间的流转必须是自由的。

看代码实现,这里简化了,

pub struct Runtime{
nodes: Arc<dyn NodeLoader> // 所有节点
...
}

// 运行一个任务的入口
// 入参:任务名,第一个节点的id,输入input
// 输出:Out
pub async fn run<In,Out>(&self,task_code:S,first_node_id:F,input:In)->anyhow::Result<Out>{
...
}

再看一下Node的抽象,很简单,每个节点只需要关注,自己能否运行,和自己如何运行。

#[async_trait::async_trait]
pub trait Node: Send + Sync {
    // 节点id
    fn id(&self) -> String;
    
    // ready: 节点是否准备好开始工作,只能判断流程节点,不用来做参数校验
    fn ready(&self, _ctx: Arc<Context>, _args: &TaskInput) -> bool {
        true
    }
    
    // 运行节点,执行特定任务
    async fn go(&self, ctx: Arc<Context>, args: TaskInput) -> anyhow::Result<TaskOutput>;
}

LLM

LLM是一切的灵魂,我们第一个实现。当然也不复杂,我们直接调用openai的开发接口。下面看一下简化代码。

pub struct LLMNode {
    id: String,
    // 大模型相关的默认参数:比如模型,tokens,temperature 等
    default_req: LLMNodeRequest,
    client: Client<OpenAIConfig>,
}

#[async_trait::async_trait]
impl rt::Node for LLMNode {
    async fn go(&self, ctx: Arc<Context>, mut args: TaskInput) -> anyhow::Result<TaskOutput> {
        let req = if let Some(query) = args.get_value::<String>() {
        ...
        } else if let Some(mut req) = args.get_value::<LLMNodeRequest>() {
        ...
        } else {
            return anyhow::anyhow!("llm: task_input is unknown").err();
        };
        //调用openai的接口,发起聊天,这里没有用stream接口
        let resp = self.chat(req).await?;
        //继续执行下一个节点
        go_next_or_over(ctx, resp)
    }
}

Tool

tool由两部分组成,

  1. tool的描述,就是我们告诉大模型我们的tool是做什么用的,包括参数是做什么。
    • 注意当参数较为复杂的时候,尽量用gpt的模型,国内的很多模型对复杂参数的拼接是极为离谱的。
  2. tool的实现,这部分我们直接用Fn trait作为一个抽象

简易代码如下:

pub struct ToolNode {
    id: String,
    // 函数描述和入参等信息
    meta: FunctionObject,
    // 函数的具体实现,为了兼容异步,函数的实际返回是个future。
    handle: Box<
        dyn Fn(String) -> Pin<Box<dyn Future<Output = anyhow::Result<String>> + Send>>
            + Send
            + Sync
            + 'static,
    >,
}

#[async_trait::async_trait]
impl Node for ToolNode {
    //兼容多种入参
    async fn go(&self, ctx: Arc<Context>, mut args: TaskInput) -> anyhow::Result<TaskOutput> {
        //如果入参是String,则直接调用handle
        if let Some(input) = args.get_value::<String>() {
            let resp = (self.handle)(input).await?;
            return go_next_or_over(ctx, resp);
        }
        //如果入参是模型给出的结果,则需要先将入参抽出来,并且返回一个聊天的消息。
        //这种方式相当于聊天中的一环
        if let Some(call) = args.get_value::<ChatCompletionMessageToolCall>() {
            let input = call.function.arguments;
            let resp = (self.handle)(input).await?;

            let msg: ChatCompletionRequestMessage = ChatCompletionRequestToolMessageArgs::default()
                .tool_call_id(call.id)
                .content(resp)
                .build()
                .unwrap()
                .into();

            return go_next_or_over(ctx, msg);
        };
        return anyhow::anyhow!("tool args error").err();
    }
}

有了ToolNode之后,我们可以实现几个默认的函数看一下效果

//查询天气的函数,入参是城市
pub(crate) fn mock_get_current_weather() -> Self {
    ToolNode::new(
        "get_current_weather",
        "Get the current weather in a given location",
        r#"{"type":"object","properties":{"location":{"type":"string","description":"the city","enum":["beijing","shanghai"]}},"required":["location"]}"#,
        Self::get_current_weather,
    )
}
//taobao的函数,能够在线购物,不过`Self::submit_order`只能购买雨伞。
pub(crate) fn mock_taobao() -> Self {
    ToolNode::new(
        "submit_order",
        "在线购物",
        r#"{"type":"object","properties":{"product":{"type":"string","description":"商品名称"}},"required":["product"]}"#,
        Self::submit_order,
    )
}

agent

有了llm+tool我们可以尝试实现一个做简单的agent。

最简agent结构图:

还是画一下最简单agent的结构图:

image.png

agent主要还是用来决策,是调用llm还是调tool,经过若干次循环后,最终给出结果。

代码实现

#[derive(Clone)]
pub struct SingleAgentNode {
    //agent的设定,按照论文的说法,这是某个领域的专家
    prompt: String,
    //工具
    tools: Vec<ChatCompletionTool>,
    //memory能够简单的存储聊天记录,这里简单抽象一下,不用关心,以后还会变。
    memory: Arc<dyn Memory>,

    id: String,
    //上下文窗口大小
    max_context_window: usize,
    //用哪个llm
    llm_model: String,
}

#[async_trait::async_trait]
impl Node for SingleAgentNode {
    async fn go(&self, ctx: Arc<Context>, mut args: TaskInput) -> anyhow::Result<TaskOutput> {
        let mut status = ctx.remove::<usize>(AGENT_EXEC_STATUS);
        if status.is_none() {
            status = Some(1);
        }
        match status.unwrap() {
            //用户query到达,调用模型
            1 => {
                ...
                return self.exec_llm(ctx);
            }
            //执行tool给出的结果
            2 => {
                ...
                Self::add_msg_to_context(ctx.clone(), resp);
                return self.exec_llm(ctx);
            }
            //调用模型后给出的回复
            3 => {
                let mut resp: CreateChatCompletionResponse = args.get_value().unwrap();
                ...
                match finish_reason.unwrap() {
                    //回复完成
                    FinishReason::Stop => {
                        return self.over(ctx, message.content.unwrap_or("无语".to_string()))
                    }
                    //模型让我们去执行tool
                    FinishReason::ToolCalls => {
                        return self.function_call(ctx, message.tool_calls.unwrap().remove(0))
                    }
                    _ => return anyhow::anyhow!("unknown finish_reason").err(),
                }
            }
            //错误
            _ => return anyhow::anyhow!("single agent unknown status").err(),
        }
        // Err(anyhow::anyhow!(""))
    }
}

对于上下文的处理。

在agent的实现中,上下文分为了两个部分,其中用户的query和最终answer作为整体上下文的内容。

而中间调用函数和函数的返回结果的部分,作为一次会话的上线文。最终不会被记录。

如下代码处理:

//在用户发问时创建当前会话的短期上下文,并且存储在Context中
if ctx.get(
    "session_context",
    |x: Option<&mut Vec<ChatCompletionRequestMessage>>| x.is_none(),
) {
    ctx.set(
        "session_context",
        Vec::<ChatCompletionRequestMessage>::new(),
    );
}

//在聊天结束后,我们将user_question和ai_response放入到memory中
self.memory.add_session_log(vec![user_question, ai_response]);

效果验证

创建3.5的llm,两个mock的tool,和一个agent放入到runtime中。启动测试

cargo test single_agent::test::test_single_agent -- --nocapture

001.gif

多智能体

关于多智能体其实就是多个agent之间的合作,文论中的实现过于繁琐,我的实现加以结合修改。使之更具备可用性。

流程&结构

看图是不是就清晰了,当query到达后,从多个agent中选出几个专家,并选出一个最相关的agent,将用户query交给他,然后他和其他专家共同探讨,最终得出一个结论。

image.png

代码实现

pub struct MultiAgent {
    //专家的id 和 专家作用的描述
    agents: Vec<(String, String)>,
    //为了能够让agent进行协作,需要给agent包装一个Node节点
    agent_tools: Vec<AgentTool>,
    //agent的向量描述,方便召回
    agent_vec: Vec<Vec<f32>>,

    id: String,
    debug: bool,
    recall_mod: RecallMod,
}

impl Node for MultiAgent {
    async fn go(&self, ctx: Arc<Context>, mut args: TaskInput) -> anyhow::Result<TaskOutput> {
        let query = args.get_value::<String>().unwrap();
        
        //召回agent,并找到最相关的agent
        let (agent, tools) = self.agent_recall(query.as_str()).await?;
        Self::add_tools_to_context(ctx.clone(), tools);
        
        //将query丢给专家团
        if self.debug {
            callback_self(ctx, self.id(), agent, query)
        } else {
            TaskOutput::new(agent, query).ok()
        }
    }
}


专家团召集

一个多智能体,最终会有非常多的专精agent,但解决一个query并不需要这么多agent,并且agent过多还会拖慢速度。所以需要召回专家。

我们这里主要用的embedding计算query和agent的余弦相似度召回。并将最相似的agent作为领导人。

注意,这种方式并不严谨,我的实际测试数据显示,准召率在90%~95%左右。并且难以优化。

应该支持多种召回方式。

// fixme 实际召回比较准的方式应该是根据 query+pe+portrait+context 进行召回,或者走个小模型
pub async fn agent_recall(&self, query: &str) -> anyhow::Result<(String, Vec<AgentTool>)> {
    match self.recall_mod {
        //这种方式,适合决策树,或者代理agent模式
        RecallMod::First => {
            let tools = self
                .agent_tools
                .iter()
                .map(|t| t.clone())
                .collect::<Vec<AgentTool>>();
            return (self.agents[0].0.clone(), tools).ok();
        }
        //指定某个agent
        //这种方式适合 agent的结构为对等拓扑,能够延续上一次记录。
        RecallMod::Specific(ref id) => {
            let tools = self
                .agent_tools
                .iter()
                .map(|t| t.clone())
                .collect::<Vec<AgentTool>>();
            return (id.clone(), tools).ok();
        }
        //embedding召回
        RecallMod::Embedding(ref n) => {
            let query_vec = embedding_small_1536(vec![query]).await?;
            let list = top_n(&query_vec[0], &self.agent_vec, *n);
            let mut tools = vec![];
            for i in list {
                if let Some(i) = self.agent_tools.get(i) {
                    tools.push(i.clone());
                }
            }
            return (tools[0].get_agent_id().to_string(), tools).ok();
        }
    }
}


验证效果

创建两个agent,一个生活agent,一个信息agent,并将他们和multi agent绑定,都放到runtime中测试:

cargo test multi_agent::test::test_multi_agent -- --nocapture


002.gif

看这个交互,能够很明显看到两个agent在相互协作完成任务。并且记忆完好。

尾语

至此,整个copilot&agent的方案基本实现完成了。通过上面两个plan的实现,其实taskflow这种固定流程的就更加简单了,正好留给看官老爷尝试一下。

关于几个细节,最后再补充一下。

  1. 为啥用的是对等式节点的设计,不用层级结构的设计?

原因很简单,为了方便扩展。上面个每个模块都是Node抽象的实现,但相互之间可以无缝调用,保持了开放能力。如果是层次机构的设计,比较呆板固定。并且会被业务限制死了。虽然实际也有用,但我不喜欢。

  1. 对于memory和pe的部分,并没有很详细的实现,会单开两篇写。

在这里插入图片描述

如何学习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%免费】🆓

在这里插入图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值