【AIGC】斯坦福小镇升级版——AI-Town源码解读

news2025/1/17 23:19:39

写在前面的话:

接上一篇斯坦福小镇升级版——AI-Town搭建指南,本本篇将解读 AI-Town 使用的技术栈、代码架构、与LLM的交互,以及与斯坦福AI小镇的对比结果(如想直接看结论可跳到文章最后)

在这里插入图片描述

整体架构


技术栈

AI-Town 使用 TypeScript/JavaScript 完成前后端全栈开发,使用的平台和工具有:

  • 游戏引擎和数据库(Game engine & Database):Convex

  • 向量数据库(VectorDB):Pinecone

  • 登录认证(Auth):Clerk

  • 文本生成模型(Text model):OpenAI

  • 部署(Deployment):Fly

  • 像素图生成(Pixel Art Generation):Replicate、Fal.ai

概念简介

项目充分地复用了许多开源项目与组件,集中在这里进行介绍,具体地:

  • Convex 是一个全栈TypeScript开发平台,用户部署应用程序时无需关心数据库与后端服务,并且默认提供了缓存与事务功能,能够实时在控制面板中查看全局数据、日志以及函数
    Convex整体是Serverless架构,为了保证一致性所有的接口函数都必须是幂等的
    在这里插入图片描述

    Convex 提供关系型数据库的功能,用户无需关心数据的存储方式,只需要通过 jsonl 或者是手动在界面上创建 Table 即可,创建完成后 Convex 会自动生成对应的 API,可以直接在 TS 代码里调用对应的 API 访问数据(太方便了TAT)
    在这里插入图片描述

    所有的 Table 及其数据都可以在 Convex 的项目控制台的 Data 页中实时观察以及修改,如想从代码里查看表结构,可阅读 convex/schema.ts 文件

  • README里有提到此项目的原型是基于这个Phaser3 项目,但源码全部重写过,有感兴趣的可以参考

  • 初始游戏地图用 PixiJS 实现,见 PixiStaticMap 组件,如需引入新地图,项目推荐的方式是在Tiled 上编辑成 csv 文件,并转成类似 convex/maps/firstmap.ts 文件中二维数组的方式导入项目(笔者注:本来以为是自动生成地图,但没有这个功能)

  • 2D相机 PixiViewport 是开源的PixiJS组件,详细实现可以看这里

  • 游戏音乐采用AI生成,在Replicate平台上运行,模型在这里,代码注释里也有标注,目前选的模型只能生成 30 秒的音乐,选择 MusicGen-Small 可以生成更长的音乐;使用者可以通过修改 convex/lib/replicate.ts 文件来重新生成音乐,也可以通过修改 convex/crons.ts 控制多久重新生成一次音乐

  • 游戏角色的像素图(应该)是通过 Fal.ai 生成的,代码里没有看到相关接口,猜测是生成后直接导入的(README里有提到用了Fal.ai)

代码分析


目录结构

package.jsonnpm run dev 实际同时启动前端 NextJS 项目和后端的 Convex 部署,两者也可以分别启动

在这里插入图片描述

具体到代码,目录虽然很多,但逻辑只需要关注 convex —— 后端代码目录、 src —— 前端NextJS代码目录即可(因为项目是Serverless架构,函数驱动的,看源码时最好把前后端看作一个整体)

NextJS 的src目录用的是 app目录模式,总共三个页面, sign-insign-up 都是引用 Clerk 提供的组件,实际只实现了一个根路由的网页; middleware.ts 也是引用了 Clerk 的自动插件;components 则是项目实现的插件
在这里插入图片描述

前端组件

这里我们主要关注根目录的 page.tsx 是如何渲染游戏画面的, page.tsx 引用了 GameWrapper 组件,调用 Game 组件,使用了 PixiViewport (开源的2D相机)、 PixiStaticMap (初始游戏地图)、 ConvexProvider (提供Convex客户端)、 Player (角色组件)等

其他组件在前面的基础概念/技术栈章节都提到过了,基本都是开源组件,如对实现方法有兴趣可阅读相关项目,这里主要关注 Player 组件

Player 组件保存每个角色的动画、位置、动作等数据,并调用 Character 组件,显示动作对应的动画

Player 能够进行的动作实际只有三个, Moving (移动)、 Speaking (交谈中)、 Thinking (思考),相比斯坦福AI小镇丰富的动作以及与地图的交互,AI-Town实际只是搭建了一个Agents与Agents交谈的框架,具体的动作状态由Convex的 playerState 参数控制

Convex函数

后端的入口在 convex/init.ts 文件,提供了几个Convex函数,包括 reset 函数(重启一个新世界)、 resetFrozen 函数(重启一个新世界并锁定时间,方便一个个迭代执行)、 seed 函数(生成一个新世界)、 addPlayers 函数(添加角色)等等,都可以独立运行以观察函数流程

// 运行单个函数
npx convex run --no-push [文件名]:[函数名]

核心数据结构

后续关键函数中会提到一些数据表,这里先描述下这几张表的用途和数据存储形式

  • memories表用于存储Agents的记忆,是用到最频繁的数据结构,但表结构其实很简单,description是这段记忆的内容,一般是文字描述、data包括这段记忆的类型以及关联的其他数据结构的id(类型有conversation-对话、reflection-反思、relationship-关系等)、playerId用于关联是属于哪个Agent的记忆、importance是对这段记忆重要度的评级
    在这里插入图片描述

  • agents表中记录了Agents的实时状态,比如是否正在思考,是否处于激活状态等,主要是一些标志位
    在这里插入图片描述

  • journal表中记录了详细的Agents行为路径
    在这里插入图片描述

关键函数

每帧的tick函数位于 convex/engine.ts 文件,每次运行将检查是否有不处于Thinking状态的 Agent,并对这些需要做出动作的 Agents 调用 agent.ts 里的 runAgentBatch 函数,这里是整个后端逻辑的主要循环(因为项目主要实现的就是Agents对话功能),整体流程如下:

首先调用 divideIntoGroups 函数,根据Agents之间的相对距离分为 多个对话组个人组,对话可以在多人之间进行,也可以通过修改代码变成 1V1对话;

对每个对话组,调用 handleAgentInteraction 函数,如果对如何实现Agents之间对话感兴趣的话,可以直接看这个函数,函数分解为如下几个步骤:

  • memories 表中寻找参与对话的Agents之间的关系( relationship )记忆,比如like代表喜欢

  • 调用 decideWhoSpeaksNext 函数——根据历史聊天记录决定下一个由谁说话(这一步将询问LLM),贴一段代码辅助理解:
    PS:这一步直接暴力拼接了超长的字符串,感觉实际生产中不能这么用…

const promptStr = `[no prose]\\n [Output only JSON]
 ${JSON.stringify(players)}
 Here is a list of people in the conversation, return BOTH name and ID of the person who should speak next based on the chat history provided below.
 Return in JSON format, example: {"name": "Alex", id: "1234"} // 让LLM给出下一步说话的人选
 ${chatHistory.map((m) => m.content).join('\\n')}`; // 这一步将拼接所有的聊天记录(超长)
// 查询样例(太长了只贴了部分)
[no prose]\\n [Output only JSON]\\n\\n  [{"agentId":"33h36nafspvkzhpvzn6xnvxk9jdy47g","characterId":"3zf2d787wd9wf4459pzems1d9jdmde0","id":"3q2vwdnnyh974nwez7v5vhrf9jdk2k8","identity":"Lucky is always happy and curious, and he loves cheese. He spends\\\\n  most of his time reading about the history of science and traveling\\\\n  through the galaxy on whatever ship will take him. He\\'s very articulate and\\\\n  infinitely patient, except when he sees a squirrel. He\\'s also incredibly loyal and brave.\\\\n  Lucky has just return'... 6985 more characters
// gpt3.5给的返回
{"name": "Kira", "id": "3ndqd9v0g7rxmt386ev9e9af9jdysb0"}
  • 调用 walkAway 函数——根据历史聊天记录决定是否离开本次对话(同样询问LLM,这里不贴详细记录了),如果有离开对话的人,则更新剩下的人的记录

  • 如果是首次开始本轮对话,调用 startConversation 函数——寻找相关的memories(这一步的token数量也爆炸),构造类似如下的prompt:

{
      role: 'user',
      content:
        `You are ${player.name}. You just saw ${newFriendsNames}. You should greet them and start a conversation with them. Below are some of your memories about ${newFriendsNames}:` +
        audience
          .filter((r) => r.relationship)
          .map((r) => `Relationship with ${r.name}: ${r.relationship}`)
          .join('\\n') +
        convoMemories.map((r) => r.memory.description).join('\\n') +
        `\\n${player.name}:`, // 拼接相关的记忆
}
  • 调用 converse 函数——实际产生对话的函数,主要的prompt构造如下,包括自己和对方的个人特征以及聊天记录(这一步选择的记忆很少,只有两条,应该是因为对话过程对LLM的调用太多了,所以缩减了token的数量,这里肯定是上下文越多效果越好的)
const relevantReflections: string =
    reflectionMemories.length > 0
      ? reflectionMemories
          .slice(0, 2)
          .map((r) => r.memory.description)
          .join('\\n')
      : ''; // 与对话相关的reflection记忆,只取两条
  const relevantMemories: string = conversationMemories
    .slice(0, 2) // only use the first 2 memories
    .map((r) => r.memory.description)
    .join('\\n'); // 与对话相关的聊天记录记忆,只取两条
// 个人设定
  let prefixPrompt = `Your name is ${player.name}. About you: ${player.identity}.\\n`;
  if (relevantReflections.length > 0) {
    prefixPrompt += relevantReflections;
    // console.debug('relevantReflections', relevantReflections);
  }
// 对方的设定和与自己的关系
  prefixPrompt += `\\nYou are talking to ${nearbyPlayersNames}, below are something about them: `;
  nearbyPlayers.forEach((p) => {
    prefixPrompt += `\\nAbout ${p.name}: ${p.identity}\\n`;
    if (p.relationship) prefixPrompt += `Relationship with ${p.name}: ${p.relationship}\\n`;
  });
// 与对话相关的记忆
  prefixPrompt += `Last time you chatted with some of ${nearbyPlayersNames} it was ${lastConversationTs}. It's now ${Date.now()}. You can cut this conversation short if you talked to this group of people within the last day. \\n}`;
  prefixPrompt += `Below are relevant memories to this conversation you are having right now: ${relevantMemories}\\n`;
  prefixPrompt +=
    'Below are the current chat history between you and the other folks mentioned above. DO NOT greet the other people more than once. Only greet ONCE. Do not use the word Hey too often. Response should be brief and within 200 characters: \\n';
  • 调用 rememberConversation 函数,总结本次谈话的内容,并存入 memories 表,这一步也询问LLM,prompt如下:
{
  role: 'user',
  content: `The following are messages. You are ${playerName}, and ${playerIdentity}
            I would like you to summarize the conversation in a paragraph from your perspective. Add if you like or dislike this interaction.`,
}
  • 对话组的prompt构造和斯坦福小镇是差不多的,都是提供对话的上下文,但是这个项目的content没有经过简化提取,token使用量巨大

对于个人组,对不进行对话的所有Agents调用调用 reflectOnMemories 函数,该函数流程如下:

  • 首先从 memories 表中获取100条该Agent最近的对话数据(conversation),以及100条最近的反应数据(reflection),合并这两个集合并以时间倒序排序选择最近的100条;计算这个100条的memory集合中的 importenceScore 总和是多少,当且仅当该值大于 500 时,才触发 reflect 的操作(这里意味着游戏运行初期将不会触发该操作)

  • reflect操作将上面得到的对话和反应数据集合作为prompt的输入
    为了方便理解,这里直接粘贴一段代码

if (shouldReflect) { // shouldReflect是对importenceScore的判断,大于500才触发
        let prompt = `[no prose]\\n [Output only JSON] \\nYou are ${name}, statements about you:\\n`;
        memories.forEach((m, idx) => { // memories变量是上面提到的100条相关数据的集合
          prompt += `Statement ${idx}: ${m.description}\\n`; // 这里没做过滤,只是把所有数据拼接在了一起
        });
        prompt += `What 3 high-level insights can you infer from the above statements?
        Return in JSON format, where the key is a list of input statements that contributed to your insights and value is your insight. Make the response parseable by Typescript JSON.parse() function. DO NOT escape characters or include '\\n' or white space in response.
          Example: [{insight: "...", statementIds: [1,2]}, {insight: "...", statementIds: [1]}, ...]`;
        console.error(prompt);
}

下面是一个实际的例子(可以看到用的token数量非常大有1w+字符,这里是笔者删减过的,实际字符数量会是3w-4w左右):

[no prose]\\n [Output only JSON] \\nYou are Alex, statements about you:\\nStatement 0: In this conversation, Kira expressed interest in getting to know Alex. Alex shared their interests in painting, programming, and sci-fi books, which Kira seemed genuinely interested in. They exchanged updates on their activities, with Alex expressing excitement about their hobbies and mentioning their love for diving into sci-fi books. The conversation took a positive turn when Kira asked for book recommendations, and Alex enth'... 14128 more characters
What 3 high-level insights can you infer from the above statements?
Return in JSON format, where the key is a list of input statements that contributed to your insights and value is your insight. Make the response parseable by Typescript JSON.parse() function. DO NOT escape characters or include '\\n' or white space in response.
Example: [{insight: "...", statementIds: [1,2]}, {insight: "...", statementIds: [1]}, ...]

而GPT对上述promt的返回如下所示(这里用的模型是 gpt-3.5-turbo-16k ):

// GPT的返回
'reflection: ' '[{"insight": "Alex enjoys conversations where the other person shows genuine interest in getting to know them and their hobbies.", "statementIds": [0, 6, 11, 16, 21]}, {"insight": "Bob\\'s lack of enthusiasm and unengaging responses make conversations with him dull and disappointing.", "statementIds": [1, 8, 23]}, {"insight": "Alex appreciates conversations that allow them to express their passions and engage in intellectual banter.", "statementIds": [3, 4, 7, 9, 10, 12, 13, 14, 15, 17, 19, 20, 22]}]'
// 以下是加入memories表中的数据
'adding reflection memory...' [
  {
    playerId: '3p48c7a4474816p4sccqngqy9jdnhe8',
    description: 'Alex enjoys conversations where the other person shows genuine interest in getting to know them and their hobbies.',
    data: {
      type: 'reflection',
      relatedMemoryIds: [ '3kbxvgxgx1kkdb1x970dxasy9je6k98', '3k2sw3zva4as0v3wfea8sf1n9jedpmg', '3kqa5ednmnnjf0cf7pnpdcvy9jech2g', '3kjb8ykh5jm4p5m00y69g4af9jecfrr', '3hqwdvc4de4nrw4d77d4x4jb9je3ax8' ]
    }
  },
  {
    playerId: '3p48c7a4474816p4sccqngqy9jdnhe8',
    description: 'Bob\\'s lack of enthusiasm and unengaging responses make conversations with him dull and disappointing.',
    data: {
      type: 'reflection',
      relatedMemoryIds: [ '3h0eseqf61meq41khgpezn479je22b8', '3kha33v6y0fyp6yce4wbr81j9je19qg', '3h9q6txm4mp53fr87bd9zmbc9jdm240' ]
    }
  },
  {
    playerId: '3p48c7a4474816p4sccqngqy9jdnhe8',
    description: 'Alex appreciates conversations that allow them to express their passions and engage in intellectual banter.',
    data: {
      type: 'reflection',
      relatedMemoryIds: [ '3k3gaky0pz8x3xyn2dqwdepd9jed6e8', '3hk9ab9ddhswc4ayqbt5gyd89je994g', '3k20ve97zeqsahvhnaexhw8a9jecdf0', '3k7q5zg02d1cvx9315sk9kwm9je91w0', '3h034dfejnat7sb7at45pspq9jeehbg', '3ge1b2vqj5xj81px8pvte7h29je2v68', '3hgx3e0pz7tv2dj71ra557j89je7n30', '3j71c478hn42kdq4r9ztqx2y9je7cfr', '3gmtpstppdcvjh2cr1vnfbtx9je88tg', '3kdf3ad3p18cns96vc0jkc2g9jefb98', '3hshm5mwv7qwykes2vjpz05h9je9p80', '3hzpbw57cwzfk3e56zg3zw6e9je8608', '3jq4ynda7axjsftbs06zmjsx9je85v0' ]
    }
  }
]

这一步的做法应该是参考了斯坦福小镇——”反思“的概念(参考这篇),这里引用文章中对于”反思“的流程描述:
在这里插入图片描述

最后,目前的实现无需等待所有的Agents返回结果,但代码里也提供了等所有Agents行动完成后再进行下一帧的方法,可自行修改

:如果想要Debug查看上面笔者演示的与LLM的交互,在Convex的Log页面不能直接看到prompt的输出,可以自己写单测来观察,下面贴一个单测的示例:

export const testMemoriesGet = internalAction({
  args:{},
  handler: async (ctx, args) => {
    const memory = MemoryDB(ctx);
    const { players, world } = await ctx.runQuery(internal.testing.getDebugPlayers);
    const p = await memory.reflectOnMemories(players[0].id, players[0].name)
  }
})

写在 convex/testing.ts 文件中,先运行 npx convex dev 部署函数,然后运行 npx convex run testing:testMemoriesGet --no-push 查看结果

花费:


笔者没有准确计算使用token的数量,

本项目目前的设定是8个Agents,仅进行交谈,不进行其他操作,3.5满速的5刀gpt接口大概能运行1个小时左右,还是挺贵的

结合前面的分析,这个花费应该是由于在构造prompt的时候没有对memory进行过滤提取,导致一次询问花费的token数量太多,这一点需要进行优化

(对比斯坦福小镇,其实两者的花销都挺高的,但这边花销的点在于扔到prompt里的content太多了,斯坦福小镇的花销来源于更多样化的策略消耗的token)

结论


AI-Town与斯坦福小镇的对比:

在这里插入图片描述

经过源码的阅读和部署实验,斯坦福小镇更像是一个论文的source code,工程化程度不高,但胜在逻辑框架清晰且有创新性;AI-Town则是工业界的项目,可扩展性高,但Agents的实现简单暴力

充分利用两者的方式应该是把斯坦福AI小镇的Agents策略迁移到AI-Town搭建的这套工作流中,应该能有更好的表现

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1008054.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

“金钥匙”转动!安全狗成功护航第二十三届中国国际投资贸易洽谈会举办

9月8日至9月11日,为期4天的第二十三届中国国际投资贸易洽谈会在厦门顺利举办。 作为国内云原生安全领导厂商,安全狗凭借突出的安全综合实力,受委托并担任此次会议网络安保技术支撑单位。 厦门服云信息科技有限公司(品牌名&#xf…

第一章 JAVA入门

文章目录 1.2 Java 的特点1.2.1 简单1.2.2 面向对象1.2.3 与平台无关① 平台与机器指令② C/C程序依赖平台③ Java 虚拟机与字节码1.2.4 多线程1.2.5 动态1.30安装 JDK1.3.1 平台简介0 Java SE②Java EE1.4 Java 程序的开发步骤②保存源文件1.5.2 编译1.8 Java之父-James Gosli…

spring boot+redis整合基础入门

文章目录 前言准备依赖项配置文件redis模板类注入设置序列化方式 实施基础字符串操作、超时设置Hash操作hash的使用场景以及优缺点 列表操作列表操作的应用场景以及优缺点 Set的基础操作Set类型的业务场景以及优缺点Demo地址 总结 前言 最近项目中有用到redis进行一些数据的缓…

【Linux】编译器 gcc/g++

1、背景知识 (1)[LMYhecs-38755 ~]$ gcc code.c -o code.exe -stdc99【-o 生成指定名字的可执行文件,-stdc99 以 C99 的标准执行程序】 (2)安装 g,yum install gcc-c 2、gcc如何完成 (1&#x…

如何使用 Node.js和Express搭建服务器?

如何使用NodeJs搭建服务器 1. 准备工作1.1 安装Node.js 2. 安装express2.1 初始化package.json2.2 安装express2.3 Express 应用程序生成器 欢迎使用Markdown编辑器新的改变功能快捷键合理的创建标题,有助于目录的生成如何改变文本的样式插入链接与图片如何插入一段…

前端面试合集(三——浏览器)

浏览器的页面渲染 1.浏览器是如何渲染页面的?2. 什么是reflow(重排)?3. 什么是repaint(重绘)?4.为什么transform效率高? 1.浏览器是如何渲染页面的? 当浏览器的网络线程收到HTML文档之后&#…

Leetcode刷题_链表相关_c++版

&#xff08;1&#xff09;92反转链表–中等 给你单链表的头指针 head 和两个整数 left 和 right &#xff0c;其中 left < right 。请你反转从位置 left 到位置 right 的链表节点&#xff0c;返回 反转后的链表 。 /*** Definition for singly-linked list.* struct Lis…

Claude: ChatGPT替代大语言模型

【产品介绍】 Claude是Anthropic推出的类ChatGPT大语言模型&#xff0c;也是一个AI人工智能助理&#xff0c;可以帮助各种行业的用户处理工作&#xff0c;如客户服务、法律、教练、搜索和销售。Claude可以通过聊天界面和API进行访问&#xff0c;能够完成各种对话和文本处理任务…

计算机二级知识点整理

翻到了之前准备计算机二级的笔记&#xff0c;现在给大家分享出来。 一、基本知识&#xff1a; 计算机把完成一条指令所花费的时间称为一个指令周期结构化程序设计强调的是程序的易读性boolean类型不能转为其他基本类型数据表达式是由运算符和运算对象构成的&#xff0c;#不是…

SimpleCG程序交互操作

前言 之前所有示例程序都是属于展示型的&#xff0c;只是作为展示板输出使用&#xff0c;不涉及键盘和鼠标的输入交互&#xff0c;下面我们开始接触具有交互功能的程序。 没有交互功能的程序可以满足一定需求,不过大部分的程序是不能脱离交互功能的。程序依据使用者的操作进行相…

Error response from daemon

文章目录 遇到的问题解决方法参考 遇到的问题 当输入下面的指令时 docker pull xxxxxx解决方法 打开/etc/docker/daemon.json文件 vim /etc/docker/daemon.json写入以下内容&#xff1a; {"registry-mirrors":["https://docker.mirrors.ustc.edu.cn"]…

OpenCV 图像像素运算操作

加法操作详解 加减乘除 #include <opencv2/opencv.hpp>using namespace cv;int main() {Mat image imread("image.jpg");if (image.empty()) {std::cout << "无法加载图像" << std::endl;return -1;}// 加法变换Mat addResult;add(ima…

mac在vscode编码过程中输入()光标在里面的时候想移出来还得动用左右键很麻烦有什么快捷方法

下载vscode插件: 想跳出大括号的时候就可以使用tab直接跳出来就行了

Buuctf web [SUCTF 2019]EasySQL

又是一道考察sql注入的题 1、起手试探 &#xff08;主要看看输入什么内容有正确的回显&#xff09; 1 0 1 1 # 发现只有在输入1的情况下有正常的回显,输入0或其他字符都没有回显&#xff0c;所以这题就要尝试堆叠注入了。 ps&#xff1a;&#xff08;如果想尝试其他注入方法…

带你打穿三层内网-红日靶场七

文章目录 前记环境配置web1信息搜集cve-2021-3129redis未授权|ssh密钥后渗透 Win7&#xff08;PC1&#xff09;永恒之蓝 web2docker逃逸 win7&#xff08;PC2&#xff09;|DC 前记 所用工具 msfcsvenomfrp蚁剑冰蝎laravel.pyfscan 注意事项 msf的永恒之蓝每次都需要两次才能…

VM-Linux基础操作命令

命令执行的本质&#xff1a; 当输入命令&#xff08;单词&#xff09;后敲击回车的那一刻。它就会立刻到以下图片&#xff0c;变量中的文件中去找对应的可执行文件 此路径又叫环境变量 1.shell命令提示符 默认&#xff1a;[rootlocalhost ~]# root&#xff1a;现已登录的账户名…

重构优化第三方查询接口返回大数据量的分页问题

# 问题描述 用户线上查询其上网流量详单数据加载慢&#xff0c;且有时候数据没有响应全~ 1、经排除是调用第三方数据量达10w条响应会超时&#xff0c;数据没正常返回 2、现有线上缓存分页也是加载慢数据不能正常展示 3、第三方接口返回类似报文jsonj&#…

基于Yolov8的光伏电池缺陷检测,引入ICCV2023 动态蛇形卷积和独家全网首发多维协作注意模块MCA,实现涨点创新十足

1.光伏电池缺陷数据集介绍 背景&#xff1a;太阳能作为一种极具吸引力的替代电力能源&#xff0c;太阳能光伏电池&#xff08;即光伏电池&#xff09;是太阳能发电系统的基础&#xff0c;一般情况下&#xff0c;电池中的各类缺陷会直接影响到光伏电池的光电转化效率和使用寿命…

Mybatis-Genertor逆向工程

1、导入mybaties插件 <build><plugins><plugin><groupId>org.mybatis.generator</groupId><artifactId>mybatis-generator-maven-plugin</artifactId><version>1.4.2</version><dependencies><dependency>…

Android Fragment

基本概念 Fragment是Android3.0后引入的一个新的API&#xff0c;他出现的初衷是为了适应大屏幕的平板电脑&#xff0c; 普通手机开发也会加入这个Fragment&#xff0c; 可以把他看成一个小型的Activity&#xff0c;又称Activity片段&#xff01; 如果一个很大的界面&#xff…