MongoDB Atlas를 LangGraph.js 와 통합하여 AI 에이전트를 구축할 수 있습니다. 이 튜토리얼은 LangGraph.js와 Atlas Vector Search를 사용하여 데이터에 대한 질문에 답변할 수 있는 에이전트를 구축하는 방법을 설명합니다.
구체적으로 다음 조치를 수행합니다.
환경을 설정합니다.
MongoDB cluster 구성합니다.
에이전트 도구를 포함하여 에이전트를 구축합니다.
에이전트에 메모리를 추가합니다.
서버를 생성하고 에이전트를 테스트합니다.
GitHub리포지토리 복제하여 이 튜토리얼의 코드로 작업합니다.
전제 조건
시작하기 전에 다음 항목이 준비되어 있는지 확인하세요.
다음 중 하나입니다.
MongoDB 버전 6.0.11 을 실행 Atlas cluster , 7.0.2 이상입니다. IP 주소 가 Atlas 프로젝트의 액세스 목록에 포함되어 있는지 확인하세요.
Atlas CLI 사용하여 생성된 로컬 Atlas 배포서버 입니다. 자세히 학습 로컬 Atlas 클러스터 배포를 참조하세요.
npm 및 Node.js가 설치되어 있어야 합니다.
Voyage AI API 키입니다. 계정과 API 키를 만들려면 Voyage AI 웹사이트 참조하세요.
OpenAI API 키입니다. API 요청에 사용할 수 있는 크레딧이 있는 OpenAI 계정이 있어야 합니다. OpenAI 계정 등록에 대해 자세히 학습 OpenAI API 웹사이트 참조하세요.
참고
이 튜토리얼에서는 OpenAI 및 Voyage AI 의 모델을 사용하지만 원하는 모델을 사용하도록 코드를 수정할 수 있습니다.
환경 설정
환경을 설정하려면 다음 단계를 완료하세요.
프로젝트 초기화하고 종속성을 설치합니다.
새 프로젝트 디렉토리 만든 다음 프로젝트 에서 다음 명령을 실행 필요한 종속 항목을 설치합니다.
npm init -y npm i -D typescript ts-node @types/express @types/node npx tsc --init npm i langchain @langchain/langgraph @langchain/mongodb @langchain/community @langchain/langgraph-checkpoint-mongodb dotenv express mongodb zod
참고
귀하의 프로젝트는 다음과 같은 구조를 사용합니다.
├── .env ├── index.ts ├── agent.ts ├── seed-database.ts ├── package.json ├── tsconfig.json
환경 파일을 생성합니다.
프로젝트 루트에 .env
파일 만들고 API 키와 연결 문자열 추가합니다.
OPENAI_API_KEY = "<openai-api-key>" MONGODB_URI = "<connection-string>" VOYAGEAI_API_KEY = "<voyage-api-key>"
<connection-string>
을(를 ) Atlas cluster 또는 로컬 Atlas 배포서버 대한 연결 문자열 로 바꿉니다.
연결 문자열은 다음 형식을 사용해야 합니다.
mongodb+srv://<db_username>:<db_password>@<clusterName>.<hostname>.mongodb.net
연결 문자열은 다음 형식을 사용해야 합니다.
mongodb://localhost:<port-number>/?directConnection=true
학습 내용은 연결 문자열을 참조하세요.
MongoDB 클러스터 구성
이 섹션에서는 데이터에 대한 벡터 검색 활성화 위해 샘플 데이터를 구성하고 MongoDB cluster 로 수집합니다.
MongoDB 에 연결할 파일 만듭니다.
MongoDB cluster 에 대한 연결을 설정하는 index.ts
파일 만듭니다.
import { MongoClient } from "mongodb"; import 'dotenv/config'; const client = new MongoClient(process.env.MONGODB_URI as string); async function startServer() { try { await client.connect(); await client.db("admin").command({ ping: 1 }); console.log("Pinged your deployment. You successfully connected to MongoDB!"); // ... rest of the server setup } catch (error) { console.error("Error connecting to MongoDB:", error); process.exit(1); } } startServer();
클러스터에 샘플 데이터를 시드합니다.
샘플 직원 기록을 생성하고 저장할 seed-database.ts
스크립트를 만듭니다. 이 스크립트는 다음 작업을 수행합니다.
직원 기록을 위한 스키마를 정의합니다.
LLM을 사용하여 샘플 직원 데이터를 생성하는 함수를 만듭니다.
각 기록 처리하여 임베딩에 사용할 텍스트 요약을 만듭니다.
LangChain MongoDB 통합을 사용하여 MongoDB cluster 벡터 저장 로 초기화합니다. 이 구성 요소는 벡터 임베딩을 생성하고
hr_database.employees
네임스페이스 에 문서를 저장합니다.
import { ChatOpenAI } from "@langchain/openai"; import { StructuredOutputParser } from "@langchain/core/output_parsers"; import { MongoClient } from "mongodb"; import { z } from "zod"; import "dotenv/config"; const llm = new ChatOpenAI({ modelName: "gpt-4o-mini", temperature: 0.7, }); const EmployeeSchema = z.object({ employee_id: z.string(), first_name: z.string(), last_name: z.string(), date_of_birth: z.string(), address: z.object({ street: z.string(), city: z.string(), state: z.string(), postal_code: z.string(), country: z.string(), }), contact_details: z.object({ email: z.string().email(), phone_number: z.string(), }), job_details: z.object({ job_title: z.string(), department: z.string(), hire_date: z.string(), employment_type: z.string(), salary: z.number(), currency: z.string(), }), work_location: z.object({ nearest_office: z.string(), is_remote: z.boolean(), }), reporting_manager: z.string().nullable(), skills: z.array(z.string()), performance_reviews: z.array( z.object({ review_date: z.string(), rating: z.number(), comments: z.string(), }) ), benefits: z.object({ health_insurance: z.string(), retirement_plan: z.string(), paid_time_off: z.number(), }), emergency_contact: z.object({ name: z.string(), relationship: z.string(), phone_number: z.string(), }), notes: z.string(), }); type Employee = z.infer<typeof EmployeeSchema>; const parser = StructuredOutputParser.fromZodSchema(z.array(EmployeeSchema)); async function generateSyntheticData(): Promise<Employee[]> { const prompt = `You are a helpful assistant that generates employee data. Generate 10 fictional employee records. Each record should include the following fields: employee_id, first_name, last_name, date_of_birth, address, contact_details, job_details, work_location, reporting_manager, skills, performance_reviews, benefits, emergency_contact, notes. Ensure variety in the data and realistic values. ${parser.getFormatInstructions()}`; console.log("Generating synthetic data..."); const response = await llm.invoke(prompt); return parser.parse(response.content as string); } async function createEmployeeSummary(employee: Employee): Promise<string> { return new Promise((resolve) => { const jobDetails = `${employee.job_details.job_title} in ${employee.job_details.department}`; const skills = employee.skills.join(", "); const performanceReviews = employee.performance_reviews .map( (review) => `Rated ${review.rating} on ${review.review_date}: ${review.comments}` ) .join(" "); const basicInfo = `${employee.first_name} ${employee.last_name}, born on ${employee.date_of_birth}`; const workLocation = `Works at ${employee.work_location.nearest_office}, Remote: ${employee.work_location.is_remote}`; const notes = employee.notes; const summary = `${basicInfo}. Job: ${jobDetails}. Skills: ${skills}. Reviews: ${performanceReviews}. Location: ${workLocation}. Notes: ${notes}`; resolve(summary); }); } const fetchEmbeddings = async (records: { pageContent: string }[]) => { const apiUrl = "https://api.voyageai.com/v1/embeddings"; const apiKey = process.env.VOYAGEAI_API_KEY; const inputs = records.map(record => record.pageContent); const requestBody = { input: inputs, model: "voyage-3.5", }; try { const response = await fetch(apiUrl, { method: "POST", headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json", }, body: JSON.stringify(requestBody), }); if (!response.ok) { throw new Error(`Error: ${response.status} ${response.statusText}`); } const data = await response.json(); return data; } catch (error) { console.error("Error while fetching embeddings:", error); } }; async function seedDatabase(): Promise<void> { try { const client = new MongoClient(process.env.MONGODB_URI as string); await client.connect(); await client.db("admin").command({ ping: 1 }); console.log("Pinged your deployment. You successfully connected to MongoDB!"); const db = client.db("hr_database"); const collection = db.collection("employees"); await collection.deleteMany({}); const syntheticData = await generateSyntheticData(); const recordsWithSummaries = await Promise.all( syntheticData.map(async (record) => ({ pageContent: await createEmployeeSummary(record), metadata: {...record}, })) ); for (const record of recordsWithSummaries ) { const db = client.db("hr_database"); const collection = db.collection("employees"); const embedding = await fetchEmbeddings([record]); const enrichedRecord = { pageContent: record.pageContent, metadata: record.metadata, embedding: embedding.data[0].embedding } const result = await collection.insertOne(enrichedRecord); console.log("Successfully added database record:", result); } await client.close(); } catch (error) { console.error("Error seeding database:", error); }} seedDatabase().catch(console.error);
시드 스크립트를 실행합니다.
npx ts-node seed-database.ts
Pinged your deployment. You successfully connected to MongoDB! Generating synthetic data... Successfully added database record: { acknowledged: true, insertedId: new ObjectId('685d89d966545cfb242790f0') } Successfully added database record: { acknowledged: true, insertedId: new ObjectId('685d89d966545cfb242790f1') } Successfully added database record: { acknowledged: true, insertedId: new ObjectId('685d89da66545cfb242790f2') } Successfully added database record: { acknowledged: true, insertedId: new ObjectId('685d89da66545cfb242790f3') }
팁
스크립트 실행 후 hr_database.employees
Atlas UI 에서 네임스페이스 로이동하여 MongoDB cluster 에서 시드된 데이터를 볼 수 있습니다.
Atlas Vector Search 인덱스를 생성합니다.
다음 단계에 따라 hr_database.employees
네임스페이스에 대한 Atlas Vector Search 인덱스를 생성합니다. 인덱스 이름을 vector_index
로 지정하고 다음 인덱스 정의를 지정합니다.
{ "fields": [ { "numDimensions": 1024, "path": "embedding", "similarity": "cosine", "type": "vector" } ] }
에이전트 구축
이 섹션에서는 에이전트의 워크플로를 조율하기 위한 그래프를 구축합니다. 그래프는 에이전트가 쿼리에 응답하기 위해 수행하는 단계 순서를 정의합니다.
agent.ts
파일 만듭니다.
프로젝트에 agent.ts
라는 이름의 새 파일을 만들고, 에이전트 설정을 시작하기 위해 다음 코드를 추가합니다. 이후 단계에서 비동기 함수에 코드를 더 추가할 것입니다.
import { ChatOpenAI } from "@langchain/openai"; import { AIMessage, BaseMessage, HumanMessage } from "@langchain/core/messages"; import { ChatPromptTemplate, MessagesPlaceholder } from "@langchain/core/prompts"; import { StateGraph } from "@langchain/langgraph"; import { Annotation } from "@langchain/langgraph"; import { tool } from "@langchain/core/tools"; import { ToolNode } from "@langchain/langgraph/prebuilt"; import { MongoDBSaver } from "@langchain/langgraph-checkpoint-mongodb"; import { MongoDBAtlasVectorSearch } from "@langchain/mongodb"; import { MongoClient } from "mongodb"; import { z } from "zod"; import "dotenv/config"; export async function callAgent(client: MongoClient, query: string, thread_id: string) { // Define the MongoDB database and collection const dbName = "hr_database"; const db = client.db(dbName); const collection = db.collection("employees"); // ... (Add rest of code here) }
에이전트 상태 정의합니다.
파일 에 다음 코드를 추가하여 그래프 상태 정의합니다.
const GraphState = Annotation.Root({ messages: Annotation<BaseMessage[]>({ reducer: (x, y) => x.concat(y), }), });
상태 에이전트 워크플로를 흐르는 데이터 구조를 정의합니다. 여기서 상태 새 메시지를 기존 대화 기록에 연결하는 리듀서를 사용하여 대화 메시지를 추적합니다.
도구를 정의합니다.
Atlas Vector Search를 사용하여 벡터 스토어 쿼리를 통해 관련 직원 정보를 조회하는 도구 및 도구 노드를 정의하려면 다음 코드를 추가하세요.
const executeQuery = async (embedding:[], n: number) => { try { const client = new MongoClient(process.env.MONGODB_URI as string); const database = client.db("hr_database"); const coll = database.collection("employees"); const agg = [ { '$vectorSearch': { 'index': 'vector_index', 'path': 'embedding', 'queryVector': embedding, 'numCandidates': 150, 'limit': n } }, { '$project': { '_id': 0, 'pageContent': 1, 'score': { '$meta': 'vectorSearchScore' } } } ]; const result = await coll.aggregate(agg).toArray(); return result } catch(error) { console.log("Error while querying:", error) } } const fetchEmbeddings = async (query: string) => { const apiUrl = "https://api.voyageai.com/v1/embeddings"; const apiKey = process.env.VOYAGEAI_API_KEY; const requestBody = { input: query, model: "voyage-3.5", }; try { const response = await fetch(apiUrl, { method: "POST", headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json", }, body: JSON.stringify(requestBody), }); if (!response.ok) { throw new Error(`Error: ${response.status} ${response.statusText}`); } const data = await response.json(); return data.data[0].embedding; } catch (error) { console.error("Error while fetching embedding:", error); } }; const employeeLookupTool = tool( async ({ query, n = 10 }) => { console.log("Employee lookup tool called"); const embedding = await fetchEmbeddings(query) const response = await executeQuery(embedding, n) const result = JSON.stringify(response) return result; }, { name: "employee_lookup", description: "Gathers employee details from the HR database", schema: z.object({ query: z.string().describe("The search query"), n: z.number().optional().default(10).describe("Number of results to return"), }), } ); const tools = [employeeLookupTool]; const toolNode = new ToolNode<typeof GraphState.State>(tools);
추가 기능을 정의합니다.
다음 코드를 추가하여 에이전트가 메시지를 처리하고 대화를 계속할지 여부를 결정하는 함수를 정의합니다.
이 함수는 에이전트 LLM을 사용하는 방법을 구성합니다.
시스템 지침 및 대화 기록이 포함된 프롬프트 템플릿을 구성합니다.
현재 시간, 사용 가능한 도구 및 메시지로 프롬프트 서식을 지정합니다.
LLM을 호출하여 다음 응답을 생성합니다.
대화 상태에 추가할 모델의 응답을 반환합니다.
async function callModel(state: typeof GraphState.State) { const prompt = ChatPromptTemplate.fromMessages([ [ "system", `You are a helpful AI assistant, collaborating with other assistants. Use the provided tools to progress towards answering the question. If you are unable to fully answer, that's OK, another assistant with different tools will help where you left off. Execute what you can to make progress. If you or any of the other assistants have the final answer or deliverable, prefix your response with FINAL ANSWER so the team knows to stop. You have access to the following tools: {tool_names}.\n{system_message}\nCurrent time: {time}.`, ], new MessagesPlaceholder("messages"), ]); const formattedPrompt = await prompt.formatMessages({ system_message: "You are helpful HR Chatbot Agent.", time: new Date().toISOString(), tool_names: tools.map((tool) => tool.name).join(", "), messages: state.messages, }); const result = await model.invoke(formattedPrompt); return { messages: [result] }; } 이 함수는 에이전트가 대화를 계속할지 종료할지 여부를 결정합니다.
메시지에 도구 호출이 포함되어 있으면 흐름을 도구 노드로 라우팅합니다.
그렇지 않으면 대화를 종료하고 최종 응답을 반환합니다.
function shouldContinue(state: typeof GraphState.State) { const messages = state.messages; const lastMessage = messages[messages.length - 1] as AIMessage; if (lastMessage.tool_calls?.length) { return "tools"; } return "__end__"; }
에이전트의 워크플로를 정의합니다.
다음 코드를 추가하여 에이전트가 쿼리에 응답하기 위해 수행하는 단계 순서를 정의합니다.
const workflow = new StateGraph(GraphState) .addNode("agent", callModel) .addNode("tools", toolNode) .addEdge("__start__", "agent") .addConditionalEdges("agent", shouldContinue) .addEdge("tools", "agent");
구체적으로 에이전트는 다음 단계를 수행합니다.
에이전트가 사용자 쿼리를 받습니다.
에이전트 노드에서 에이전트는 쿼리를 처리하고 도구를 사용할지 대화를 종료할지 결정합니다.
도구가 필요한 경우 에이전트는 도구 노드로 라우팅하여 선택한 도구를 실행합니다. 도구의 결과는 다시 에이전트 노드로 전송됩니다.
에이전트는 도구의 출력을 해석하여 응답을 생성하거나 다음 작업을 결정합니다.
이 과정은 에이전트가 추가 작업이 필요하지 않다고 판단할 때까지 계속됩니다(
shouldContinue
함수가end
를 반환할 때까지).
에이전트에 메모리 추가
에이전트의 성능을 향상시키기 위해 MongoDB 체크포인트를 사용하여 상태를 지속할 수 있습니다. 지속성은 에이전트가 이전 상호작용에 대한 정보를 저장할 수 있도록 하며 이를 통해 에이전트는 향후 상호작용에서 더욱 맥락적으로 관련된 응답을 제공할 수 있습니다.
에이전트 기능을 완료합니다.
마지막으로, 쿼리를 처리하는 에이전트 기능을 완성하기 위해 다음 코드를 추가합니다.
const finalState = await app.invoke( { messages: [new HumanMessage(query)], }, { recursionLimit: 15, configurable: { thread_id: thread_id } } ); console.log(finalState.messages[finalState.messages.length - 1].content); return finalState.messages[finalState.messages.length - 1].content;
서버 생성 및 에이전트 테스트
이 섹션에서는 에이전트와 상호 작용하며 기능을 테스트할 서버를 만듭니다.
Express.js 서버 구성합니다.
index.ts
파일을 다음 코드로 바꿉니다.
import 'dotenv/config'; import express, { Express, Request, Response } from "express"; import { MongoClient } from "mongodb"; import { callAgent } from './agent'; const app: Express = express(); app.use(express.json()); const client = new MongoClient(process.env.MONGODB_URI as string); async function startServer() { try { await client.connect(); await client.db("admin").command({ ping: 1 }); console.log("Pinged your deployment. You successfully connected to MongoDB!"); app.get('/', (req: Request, res: Response) => { res.send('LangGraph Agent Server'); }); app.post('/chat', async (req: Request, res: Response) => { const initialMessage = req.body.message; const threadId = Date.now().toString(); try { const response = await callAgent(client, initialMessage, threadId); res.json({ threadId, response }); } catch (error) { console.error('Error starting conversation:', error); res.status(500).json({ error: 'Internal server error' }); } }); app.post('/chat/:threadId', async (req: Request, res: Response) => { const { threadId } = req.params; const { message } = req.body; try { const response = await callAgent(client, message, threadId); res.json({ response }); } catch (error) { console.error('Error in chat:', error); res.status(500).json({ error: 'Internal server error' }); } }); const PORT = process.env.PORT || 3000; app.listen(PORT, () => { console.log(`Server running on port ${PORT}`); }); } catch (error) { console.error('Error connecting to MongoDB:', error); process.exit(1); } } startServer();
에이전트를 테스트합니다.
에이전트와 상호 작용하기 위한 샘플 요청을 보내세요. 응답은 사용 중인 데이터와 모델에 따라 달라질 수 있습니다.
참고
요청은 JSON 형식으로 응답을 반환합니다. 서버가 실행 중인 터미널에서 일반 텍스트 출력을 볼 수도 있습니다.
curl -X POST -H "Content-Type: application/json" -d '{"message": "Build a team to make a web app based on the employee data."}' http://localhost:3000/chat
# Sample response {"threadId": "1713589087654", "response": "To assemble a web app development team, we ideally need..." (truncated)} # Plaintext output in the terminal To assemble a web app development team, we ideally need the following roles: 1. **Software Developer**: To handle the coding and backend. 2. **UI/UX Designer**: To design the application's interface and user experience. 3. **Data Analyst**: For managing, analyzing, and visualizing data if required for the app. 4. **Project Manager**: To coordinate the project tasks and milestones, often providing communication across departments. ### Suitable Team Members for the Project: #### 1. Software Developer - **John Doe** - **Role**: Software Engineer - **Skills**: Java, Python, AWS - **Location**: Los Angeles HQ (Remote) - **Notes**: Highly skilled developer with exceptional reviews (4.8/5), promoted to Senior Engineer in 2018. #### 2. Data Analyst - **David Smith** - **Role**: Data Analyst - **Skills**: SQL, Tableau, Data Visualization - **Location**: Denver Office - **Notes**: Strong technical analysis skills. Can assist with app data integration or dashboards. #### 3. UI/UX Designer No specific UI/UX designer was identified in the current search. I will need to query this again or look for a graphic designer with some UI/UX skills. #### 4. Project Manager - **Emily Davis** - **Role**: HR Manager - **Skills**: Employee Relations, Recruitment, Conflict Resolution - **Location**: Seattle HQ (Remote) - **Notes**: Experienced in leadership. Can take on project coordination. Should I search further for a UI/UX designer, or do you have any other parameters for the team?
이전 응답에서 반환된 스레드 ID를 사용하여 대화를 계속할 수 있습니다. 예를 들어, 후속 질문을 하려면 다음 명령을 사용하세요. <threadId>
를 이전 응답에서 반환된 스레드 ID로 바꿉니다.
curl -X POST -H "Content-Type: application/json" -d '{"message": "Who should lead this project?"}' http://localhost:3000/chat/<threadId>
# Sample response {"response": "For leading this project, a suitable choice would be someone..." (truncated)} # Plaintext output in the terminal ### Best Candidate for Leadership: - **Emily Davis**: - **Role**: HR Manager - **Skills**: Employee Relations, Recruitment, Conflict Resolution - **Experience**: - Demonstrated leadership in complex situations, as evidenced by strong performance reviews (4.7/5). - Mentored junior associates, indicating capability in guiding a team. - **Advantages**: - Remote-friendly, enabling flexible communication across team locations. - Experience in managing people and processes, which would be crucial for coordinating a diverse team. **Recommendation:** Emily Davis is the best candidate to lead the project given her proven leadership skills and ability to manage collaboration effectively. Let me know if you'd like me to prepare a structured proposal or explore alternative options.