前言
上篇文章我们大致演示了一下ai_agent
的食用方法。这里我们做一下核心模块runtime
的设计和实现。
一个agent也好,workflow也好,他们单个实现起来并不复杂,困难的是如何将他们有机的组合起来,能够按照一定的逻辑流转起来。并且能够层层嵌套,能力无限。
理解起来比较抽象,我们先看几个重点方向,和现实中的例子:
1. 简单的workflow场景
我们可以使用一个简单的workflow来处理一些场景,比如奶茶店的智能推荐,那么它的一个流程大致如下:
graph LR
User --用户画像+query+上下文+其他--> pormpt
pormpt --> llm
llm --> answer
answer --> 奶茶卡片
- 这个流程就能看到,我们将llm,memory,prompt都当做一个节点,按照图中的顺序执行最终会得到一个推荐结果。
- 可以说这是一个workflow的基础功能。
2. smartflow场景
对于有些问题,我们是不能够将完全预测出执行流程,比如Least-to-Most
和smartflow
的场景,这些节点往往是在执行过程中慢慢生成出来的。
我们还是举个例子, 假设有这样一个agent:评阅大师,它的目标是评阅优化输入文案。那么它的工作流程可能是这样的。
graph TD
user -->|文案| pf[评分llm]
pf -->|评分>80| 输出
pf -->|评分<80 \n 待优化方向| yhp[追加上下文]
yhp --> yh[优化llm]
yh --> pf
- 工作方式一目了然,可以理解为一个评分llm,一个优化llm。对于一个文案,评分llm不断给出需要优化的点,并追加到上下文中,优化llm则根据要求不断优化,直到评分>80。
- 我们的流程设计肯定不能是DAG,因为出现环了。
3. multi-agent场景
multi-agent
常用来解决复杂问题,对流程设计的要求也更高。比较典型的场景是狼人杀。它的流程可能长这样。
graph TD
xdyt[入夜] --> zcr
zcr[主持人] -->|第一轮| lrfy[狼人开刀]
lrfy --> zcr
zcr[主持人agent] -->|第二轮| nwfy[女巫药]
nwfy --> zcr
zcr[主持人agent] -->|第三轮| yyjfy[预言家查人]
yyjfy --> zcr
zcr[主持人agent] -->|第四轮| syrfy[所有人发言]
syrfy --> gp[归票]
gp --> zcr
zcr -->|某个阵容胜利| 结束
- 上图画的游戏规则并不严谨,但大体能感受到一个
multi-agent
的工作方式。这里面的每个角色都可以理解为一个agent,主持人可以是一个固定的脚本。 - 这个场景要求每个agent都有自己的prompt,有独立的memory等,将一个agent放大,那么这个agent也应该是一个worlflow,由无数的节点拼接而来。也就要求我们流程设计能够层层嵌套,能力无限。
4. NL2code场景
保持开放是一个非常重要的能力,一来是能够让程序员直接写脚本。二是大模型有时候也会自己写脚本,也就是NL2Code
场景。
我之前参与过一个专项,其中的一个能力是用户自由操作文档,比如文档归类,概要摘取等,结果通过text2sql
存到数据库中。
这种场景下,仅仅提供有限功能的节点是不足够的。必须具备无限扩展的能力。
5. 开放域
在开放域中,agent将具备更自由的意志。举几个典型的场景:聊天室
游戏NPC
虚拟宠物
在这种应用中,agent更应该是一个完备的ai,具备更长的生命周期,更强的主观能动性。
agent从运行到结束,要像人一样,生下来就有意识,直到死亡。
简单的做法是先构建一个single agent,并让它至少有个memory+tool+设定这几个模块,最好是采用多模态大模型做基座模型。然后不断循环调用这个agent,从而达到和我一样的牛马状态。当然成本肯定极高。
另一种方法是做双循环,还是这个agent,一个循环是外界有输入后再调用agent,另一个循环是定时唤起agent的主观意识。从而节能减排。
不管是哪种方式,我更倾向于从agent外部解决。而非agent本身。
总结
现实中应用肯定不局限于这几种情况,但通过一定的流程编辑基本都可以解决,只是复杂性会比较高。
runtime 设计
一个agent的runtime大概可以看成是由这几部分构成:调度+节点服务+执行计划+上下文。
节点 service
一个service可以理解成一个原子能力,比如llm,比如tool,也可能是function。
我们这里做一个抽象:
pub trait Service: Send + Sync {
async fn call(&self, flow: Flow) -> anyhow::Result<Output>;
}
执行计划
这个也很好理解,我们需要按照这个计划不断执行service。
pub trait Plan: Send + Sync {
fn next(&self, ctx: Arc<Context>, node_id: &str) -> NextNodeResult;
fn set(&self, nodes: Vec<PlanNode>);
fn update(
&self,
node_code: &str,
update: Box<dyn FnOnce(Option<&mut PlanNode>) -> anyhow::Result<()>>,
) -> anyhow::Result<()>;
}
上下文Context
这个所谓的上下文Context
可以理解为 执行一个任务的具体消息,比如任务编码,堆栈信息,状态等。
- 这里有一个子上下文的概念,也就是说在一个
service
里面可以执行一个子计划,也就是能力无限的实现。并且父子上下文会共享堆栈。todo:也会共享状态。 - 这里没有做共享设计,每个
Context
会记录自己归属的运行时。也就是每个运行时都是独立的,为了多租户设计的。
pub struct Context {
pub parent_code: Option<String>,
//任务流名称
pub code: String,
//状态
pub status: AtomicU8, //0:init 1:running, 2:success, 3:error
//堆栈信息
pub stack: Arc<Mutex<ContextStack>>,
//执行计划
pub plan: Arc<dyn Plan>,
//全局扩展字段
pub extend: Mutex<HashMap<String, Box<dyn Any + Send + Sync + 'static>>>,
//结束时回调
pub over_callback: Option<Mutex<Vec<Box<dyn FnOnce(Arc<Context>) + Send + Sync + 'static>>>>,
pub(crate) runtime: Arc<Runtime>,
}
调度
调度也很简单,就是按照plan
不断执行service
- 这里做了中间层设计,也就是每个service都是一个‘洋葱’,一层层进,一层层出。
- 每个service都是异步执行的,就是说如果你的
plan
存在并行结构,那么两个并行的分支会一起执行。
fn exec_next_node(ctx: Arc<Context>, node_code: &str) {
let nodes = ctx.plan.next(ctx.clone(), node_code);
...
for i in nodes {
match ctx.runtime.nodes.get(i.node_type_id.as_str()) {
...
};
//将service和middle封装成一个flow执行
let flow = Flow::new(i, ctx.clone(), middle);
tokio::spawn(async move {
let ctx = flow.ctx.clone();
let result = tokio::spawn(async move {
... //处理一些堆栈信息
//执行flow
if let Err(e) = flow.call().await {
//错误处理
} else {
//执行成功则继续执行下一个
Runtime::exec_next_node(ctx, code.as_str());
}
})
.await;
... //检查错误,和故障恢复
});
}
}
Service layer
为了方便service的实现,我们将service再包装一层。如下:
pub trait ServiceLayer: Sync + Send {
//service的配置类型
type Config;
//输出结果
type Output;
//执行过程
async fn call(
&self,
code: String,
ctx: Arc<Context>,
cfg: Self::Config,
) -> anyhow::Result<Self::Output>;
}
再为这个包装层加一个自动json解析的实现。这样入参和出参可以直接绑定到struct。
impl<T, In, Out> Service for LayerJson<T, In, Out>
where
T: ServiceLayer<Config = In, Output = Out>,
In: for<'a> Deserialize<'a> + Send + Sync,
Out: Serialize + Send + Sync,
{
async fn call(&self, flow: Flow) -> anyhow::Result<Output> {
... //类型解析和参数的初步处理
//解析入参
let cfg = match serde_json::from_str::<In>(node_config.as_str()) {
... //错误处理
};
//调用执行过程
let output = self.handle.call(code, ctx, cfg).await ? ;
//解析出参
let raw = match serde_json::to_value(&output) {
Ok(o) => o,
Err(e) => return anyhow::anyhow!("code[{}],output json error:{}", node_info, e).err(),
};
Output::new(raw).raw_to_ctx().ok()
}
}
如何学习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%免费】🆓