身為一個內向的宅宅,我想大家應該都懂比起跟人聊天,我們更喜歡看人們聊天。
所以我常在想有沒有辦法讓AI們自己玩,會不會出現什麼意想不到的對話!
於是~我就做了這個 雙 AI 對話模擬器 。
這個專案基於 Vue3、 Vite、daisyUI,並使用 Google Gemini AI API,讓 AI 角色能夠彼此交流。你可以設定兩個 AI 的名字和個性描述,然後它們就會開始一場酣暢淋漓的交流。
💬 雙 AI 對話: 讓兩個 AI 角色自行對話,展現 AI 的對話能力。
🎭 角色設定:使用者可以自定義 AI 角色的名稱和描述。
🎲 隨機生成角色:沒有靈感?沒關係,也可以隨機產生角色設定!
🤖 Gemini AI API:使用 Google Gemini AI API 進行對話生成。
到 Google AI Studio 註冊並取得 API 金鑰。
npm create vite@latest my-gemini-ai-project --template vue
cd my-gemini-ai-project
npm install
npm install @google/generative-ai
建立 src/components/AIAgentConfig.vue,讓使用者可以設定兩個 AI 角色的名稱和描述,使用 v-model 實現雙向綁定。
<script setup>
import { ref } from "vue";
import { generateRandomName, generateRandomDescription } from "../utils/random";
const agent1 = ref({ name: "", description: "" });
const agent2 = ref({ name: "", description: "" });
const emit = defineEmits(["start-conversation"]);
const startConversation = () => {
if (
agent1.value.name === "" ||
agent1.value.description === "" ||
agent2.value.name === "" ||
agent2.value.description === ""
)
return;
emit("start-conversation", { agent1: agent1.value, agent2: agent2.value });
};
const generateRandomAgents = async () => {
agent1.value.name = await generateRandomName();
agent1.value.description = await generateRandomDescription();
agent2.value.name = await generateRandomName();
agent2.value.description = await generateRandomDescription();
};
</script>
<template>
<div class="flex flex-col items-center">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 w-4/5">
<div className="flex flex-col gap-4">
<h2 className="text-5xl font-bold">AI角色 1</h2>
<label className="form-control w-full">
<div className="label">
<span className="label-text">名稱</span>
</div>
<input
type="text"
className="input input-bordered w-full"
id="agent1Name"
v-model="agent1.name"
/>
</label>
<label className="form-control">
<div className="label">
<span className="label-text">描述</span>
</div>
<textarea
className="textarea textarea-bordered h-24"
id="agent1Description"
v-model="agent1.description"
></textarea>
</label>
</div>
<div className="flex flex-col gap-4">
<h2 className="text-5xl font-bold">AI角色 2</h2>
<label className="form-control w-full">
<div className="label">
<span className="label-text">名稱</span>
</div>
<input
type="text"
className="input input-bordered w-full"
id="agent2Name"
v-model="agent2.name"
/>
</label>
<label className="form-control">
<div className="label">
<span className="label-text">描述</span>
</div>
<textarea
className="textarea textarea-bordered h-24"
id="agent2Description"
v-model="agent2.description"
></textarea>
</label>
</div>
</div>
<div class="flex gap-2 mt-5">
<button @click="generateRandomAgents" class="btn btn-accent btn-wide">
隨機生成
</button>
<button @click="startConversation" class="btn btn-info btn-wide">
開始對話
</button>
</div>
</div>
</template>
<style scoped></style>
在 src/utils/random.js 中,使用 Gemini API 來產生隨機的角色名稱和描述:
import { generateContent } from "./gemini";
export const generateRandomName = async () => {
const prompt = "請隨機生成一個現實生活中會出現的角色的名稱";
try {
const response = await generateContent(prompt);
return response.trim().replace(/"/g, '');
} catch (error) {
console.error("Error generating random name:", error);
return "Error Name"
}
};
export const generateRandomDescription = async () => {
const prompt = "請隨機生成一個現實生活中會出現的角色的描述,長度約 20 字左右";
try {
const response = await generateContent(prompt);
return response.trim().replace(/"/g, '');
} catch (error) {
console.error("Error generating random description:", error);
return "Error Description"
}
};
在 src/utils/gemini.js 中,設定 API 金鑰、安全設定,並封裝呼叫 Gemini API 的邏輯:
import {
GoogleGenerativeAI,
HarmBlockThreshold,
HarmCategory,
} from "@google/generative-ai";
const genAI = new GoogleGenerativeAI(import.meta.env.VITE_GEMINI_API_KEY);
// 設定安全設定,這裡只是範例,請根據你的需求調整
const safetySettings = [
{
category: HarmCategory.HARM_CATEGORY_HATE_SPEECH,
threshold: HarmBlockThreshold.BLOCK_ONLY_HIGH, // 只封鎖高風險仇恨言論
},
{
category: HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT,
threshold: HarmBlockThreshold.BLOCK_ONLY_HIGH, // 只封鎖高風險色情內容
},
{
category: HarmCategory.HARM_CATEGORY_HARASSMENT,
threshold: HarmBlockThreshold.BLOCK_ONLY_HIGH, // 只封鎖高風險騷擾
},
{
category: HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT,
threshold: HarmBlockThreshold.BLOCK_MEDIUM_AND_HIGH, // 封鎖中高風險危險內容
},
];
const model = genAI.getGenerativeModel({
model: "gemini-pro",
safetySettings,
});
export async function generateContent(prompt) {
try {
const result = await model.generateContent(prompt);
const response = await result.response;
const text = response.text();
return text;
} catch (error) {
console.error("Error in generateContent:", error);
throw new Error(`Gemini API Error: ${error.message}`);
}
}
記得在 .env 或 .env.local 檔案中設定你取得的 Google Gemini API 金鑰:
VITE_GEMINI_API_KEY=YOUR_GEMINI_API_KEY
我們修改 src/App.vue,引入 AIAgentConfig 元件,並實作對話邏輯:
<script setup>
import { ref } from "vue";
import AIAgentConfig from "./components/AIAgentConfig.vue";
import { generateContent } from "./utils/gemini";
import { twMerge } from "tailwind-merge";
const conversation = ref([]);
const isConversationRunning = ref(false);
const agents = ref({});
const context = ref("他們正在聊天");
const handleStartConversation = async (agentsData) => {
console.log("agents", agentsData);
agents.value = agentsData;
conversation.value = [];
isConversationRunning.value = true;
let currentSpeaker = "agent1";
const { agent1, agent2 } = agentsData;
try {
while (isConversationRunning.value) {
const prompt = `
${agent1.name}的描述:${agent1.description}
${agent2.name}的描述:${agent2.description}
${currentSpeaker === 'agent1' ? agent1.name : agent2.name}現在要說話, 請回傳下一句話, 請注意回傳的格式為 "回覆內容"
請不要有其他多餘的文字
目前對話內容:
${conversation.value.map(msg => `${msg.sender}:${msg.text}`).join('\n')}
`;
const response = await generateContent(prompt);
conversation.value.push({
sender: currentSpeaker === "agent1" ? agent1.name : agent2.name,
text: response,
});
currentSpeaker = currentSpeaker === "agent1" ? "agent2" : "agent1";
}
} catch (error) {
console.error("Error generating conversation:", error);
}
};
// 停止對話
const stopConversation = () => {
isConversationRunning.value = false;
};
</script>
<template>
<div class="container mx-auto p-4">
<AIAgentConfig @start-conversation="handleStartConversation" />
<div class="mt-4 w-4/5 mx-auto">
<div v-if="conversation.length > 0">
<div
v-for="(message, index) in conversation"
:key="index"
:class="twMerge('chat', index % 2 === 0 ? 'chat-start' : 'chat-end')"
>
<div className="chat-header">
{{ message.sender }}
</div>
<div className="chat-bubble">
{{ message.text }}
</div>
</div>
</div>
<div v-if="isConversationRunning">
<span className="loading loading-spinner loading-lg block"></span>
<button @click="stopConversation" class="btn btn-error mt-4 mx-auto">
停止對話
</button>
</div>
</div>
</div>
</template>
<style scoped></style>
在過程中,我遇到的一些狀況,來分享一下:
要讓 AI 產生更自然、生動的對話,就需要設計有效的 prompt。我嘗試了不同的 prompt 結構,有結構化、情境提示、也有平舖直述式的 prompt,不過有時候對話還是會出現怪怪的現象。
Gemini API 為了避免產生不當內容,有安全過濾機制,但是身為人類就是想看不正經的對話,所以我去設定安全過濾的門檻,但發現官方文件上說不能設置BLOCK_NONE
(也就是完全不封鎖、不過濾),所以最後只能選擇BLOCK_ONLY_HIGH
。
以下是一些可用的 HarmCategory
:
HarmCategory.HARM_CATEGORY_HATE_SPEECH
: 仇恨言論HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT
: 色情內容HarmCategory.HARM_CATEGORY_HARASSMENT
: 騷擾HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT
: 危險內容每個 HarmCategory
可設定以下的 SafetySetting
:
BLOCK_NONE
: 完全不封鎖BLOCK_ONLY_HIGH
: 只封鎖高風險內容BLOCK_MEDIUM_AND_HIGH
: 封鎖中高風險內容BLOCK_LOW_AND_ABOVE
: 封鎖低、中高風險內容好了,現在我們有了一個雙 AI 對話模擬器。現在就試試這個 Gemini AI 雙 AI 對話模擬器,看看兩個 AI 會聊出什麼神奇的內容吧!如果有生成甚麼有趣的對話也可以在留言區跟我分享喔~ ( 我超想看的