Практычныя нататкі: карпаратыўны AI-памочнік на RAG, агентах і LangGraph
Практычны разбор тэмы «карпаратыўны AI-памочнік на RAG, агентах і LangGraph»: кантракты, праверкі і гатовыя фрагменты кода для каманд, якія ўкараняюць гэты падыход.
Крыніцы: Building a Company AI Assistant with RAG, Agents, and LangGraph
Наступныя прытамкі паказваюць практычны шлях для стварэння AI-асістэнта для компаній за дапамою RAG, агентаў і LangGraph. Акцэнт ставіцца на контракты, перакананняя і мескі для коду, а не на мотывацыйныя аспекты. Калі працуеце над адглядам, спачатку запісайце контракт: неабяжныя даны, сігнал успеху і тое, што вядзецца пад частым нявыпаннем. Такі спіс перакананняя дапамагае залішыцца чыстым пад будучымі зменамі коду. Валідзіце маленькія, тэставаныя елементы заместо велікіх скрыптаў. Калі якісь крок не выйшоў, прычына нявыпанню должна вказываць на адну конкрэтную адпаведальнасць, а не на заплутаны працэс.
rag-agent-project/
│
├── documents/
├── faiss_index/
│
├── .env
├── .gitignore
├── requirements.txt
│
├── config.py
├── ingest.py
├── rag.py
├── query_rewriter.py
├── reranker.py
├── tools.py
├── graph.py
├── main.py
└── app.py
pip install -r requirements.txt
.env
OPENAI_API_KEY=your_openai_api_key
import os
from dotenv import load_dotenv
load_dotenv()
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
if not OPENAI_API_KEY:
raise ValueError("OPEN_API_KEY is not set."
"Add it to yor .env file.")
LLM_MODEL = "gpt-4.1-mini"
EMBEDDING_MODEL = "text-embedding-3-small"
HUGGINGFACE_EMBEDDING = "sentence-transformers/all-MiniLM-L6-v2"
FAISS_PATH = "faiss_index"
DATABASE_PATH = "company_assistant.db"
documents/
├── employee_handbook.pdf
├── hr_policy.pdf
└── it_policy.pdf
from pathlib import Path
from langchain_community.document_loaders import PyPDFLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_community.vectorstores import FAISS
from langchain_huggingface import HuggingFaceEmbeddings
from config import HUGGINGFACE_EMBEDDING , FAISS_PATH
DOCUMENTS_DIR = Path("documents")
def load_documents():
all_documents = []
pdf_files = list(DOCUMENTS_DIR.glob("*.pdf"))
if not pdf_files:
raise FileNotFoundError(
"No pdf file found in documents/"
)
for pdf_file in pdf_files:
print(f"Loading: {pdf_file}")
loader = PyPDFLoader(str(pdf_file))
documents = loader.load()
for document in documents:
document.metadata["source"] = pdf_file.name
if "handbook" in pdf_file.name.lower():
document.metadata["document_type"] = "handbook"
elif "policy" in pdf_file.name.lower():
document.metadata["document_type"] = "policy"
else:
document.metadata["document_type"] = "other"
all_documents.append(document)
return all_documents
def main():
# 1. Load PDFs
documents = load_documents()
print(f"\nLoaded {len(documents)} pages")
# 2. Split documents
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=500,
chunk_overlap=100
)
chunks = text_splitter.split_documents(documents)
print(f"Split into {len(chunks)} chunks.")
# 3. Create embeddings
embeddings = HuggingFaceEmbeddings(
model_name=HUGGINGFACE_EMBEDDING
)
# 4. Create FAISS database
vector_store = FAISS.from_documents(
chunks,
embeddings
)
# 5. Save FAISS database
vector_store.save_local(FAISS_PATH)
print(
f"\nFAISS index saved to: {FAISS_PATH}"
)
if __name__ == "__main__":
main()
Loading: documents/employee_handbook.pdf
Loading: documents/hr_policy.pdf
Loading: documents/it_policy.pdf
Loaded 20 pages.
Created 60 chunks.
FAISS index created successfully!
Chunk 1
-------------------------
A B C D E F G H I J
|
| overlap
v
Chunk 2
E F G H I J K L M N
from langchain_huggingface import HuggingFaceEmbeddings
from langchain_community.vectorstores import FAISS
from config import(
HUGGINGFACE_EMBEDDING,
FAISS_PATH
)
embeddings = HuggingFaceEmbeddings(
model_name=HUGGINGFACE_EMBEDDING
)
vector_store = FAISS.load_local(
FAISS_PATH,
embeddings,
allow_dangerous_deserialization=True
)
from rag import vector_store
from langchain_huggingface import (HuggingFaceEmbeddings,HuggingFacePipeline)
from transformers import pipeline
pipe = pipeline(
"text-generation",
model="Qwen/Qwen2.5-0.5B-Instruct",
max_new_tokens=256,
temperature=0.1
)
llm = HuggingFacePipeline(
pipeline=pipe
)
def rewrite_query(question:str)-> str:
prompt = f"""
Rewrite the user's question into a concise search query for company document database.
The query should contain important keywords that are likely to appear in company documents.
Do not answer the question.
User question:{question}
search query:
"""
response = llm.invoke(prompt)
return response
Chunk 1
Chunk 2
Chunk 3
Chunk 4
Chunk 5
from langchain_huggingface import (HuggingFaceEmbeddings,HuggingFacePipeline)
from transformers import pipeline
pipe = pipeline(
"text-generation",
model="Qwen/Qwen2.5-0.5B-Instruct",
max_new_tokens=256,
temperature=0.1
)
llm = HuggingFacePipeline(
pipeline=pipe
)
def rerank_documents(query:str, documents:list,top_n: int = 3):
scored_documents =[]
for document in documents:
prompt = f"""
You are evaluating document releveance.
Rate how relevant the document is to the user's question.
Return ONLY an intefer from 0 to 10.
0 = competely irrelevent
10 = extreamly relevent
Question: {query}
Document:{document.page_content}
"""
response = llm.invoke(prompt)
try:
score = int(response.content.strip())
except ValueError:
score = 0
scored_documents.append((score,document))
#Highest score first
scored_documents.sort(
key=lambda item:item[0],
reverse=True
)
return[
document
for score, document
in scored_documents[:top_n]
]
from langchain.tools import tool
from rag import vector_store
from query_rewriter import(rewrite_query)
from reranker import (rerank_documents)
@tool
def company_document_search(query: str) -> str:
"""
Search company PDF documents for information about
company policies, employee benifits, leave, holidays, work from home,
IT rules, working hours, and othher internal information.
"""
#1. Rewrite Query
imporved_query = rewrite_query(query)
print(f"\n[Rewritten Query]"
f"\n{imporoved_query}")
#2. Vector serach
documents = (vector_store.similarity_search(
imporved_query,
k=5
))
if not documents:
return(
"No relevent information"
"was found in the company documents"
)
#Reranking
documents = rerank_documents(
imporved_query, documents,top_n=3
)
#4.Formate context
result = []
for document in documents:
source = document.metadata.get("source","Unknown")
page = document.metadata.get("page","Unknown")
#PyPDFLoader user zero-based pages
if isinstance(page, int):
page = page+1
results.append(f"""
SOURCE:{source}
PAGE:{page}
CONTENT: {document.page_content}
""")
return "\n".join(result)
@tool
def calculator(expression: str) -> str:
"""
Calculate a mathematical expression.
"""
try:
# result = eval(expression)
# for learning only
# Do not use eval() in production
allowed = ("0123456789""+-*/(). ")
if not all(char in allowed for char in expression):
return ("Invalid mathmatical expression")
result = eval(expression,
{"__builtins__":{}},
{}
)
return str(result)
except Exception:
return "Unable to calculate the expression."
from langchain_ollama import ChatOllama
from langgraph.graph import (
StateGraph,
MessagesState,
START
)
from langgraph.prebuilt import (
ToolNode,
tools_condition
)
from langgraph.checkpoint.memory import InMemorySaver
from tools import (
company_document_search,
calculator
)
# Local Qwen model
llm = ChatOllama(
model="qwen2.5:3b",
temperature=0
)
# Tools
tools = [
company_document_search,
calculator
]
llm_with_tools = llm.bind_tools(tools)
# Agent
def agent_node(state: MessagesState):
response = llm_with_tools.invoke(
state["messages"]
)
return {
"messages": [response]
}
# Graph
graph_builder = StateGraph(MessagesState)
graph_builder.add_node(
"agent",
agent_node
)
graph_builder.add_node(
"tools",
ToolNode(tools)
)
# START → Agent
graph_builder.add_edge(
START,
"agent"
)
# Agent → Tool OR END
graph_builder.add_conditional_edges(
"agent",
tools_condition
)
# Tool → Agent
graph_builder.add_edge(
"tools",
"agent"
)
# Checkpointer
checkpointer = InMemorySaver()
# Compile
graph = graph_builder.compile(
checkpointer=checkpointer
)
SOURCE: hr_policy.pdf
PAGE: 4
Source: hr_policy.pdf, page 4.
from langchain_core.messages import HumanMessage, SystemMessage
from graph import graph
SYSTEM_PROMPT="""
You are an intelligent company assistant.
You answer question using company documents and avalable tools.
Rules:
1. Use company_document_search whenever the user asks about company-specific information.
2. Never invent company policies, benifits, rules, or procedures.
3. Use the calculator when mathmatical calculation are required.
4. When company documents are used, mention the source document and page.
5. If the company documents do not contain the requested information, clearly say
that you could not find it.
6. Use previous conversation context to undersatnd follow-up questions.
7. Give concise and clear answer
"""
def chat(
question:str,
thread_id: str):
config = {
"configurable":{"thread_id":thread_id}
}
#Check whether thhis conversation already exists
state = graph.get_state(config)
if not state.values:
messages = [
SystemMessage(content=SYSTEM_PROMPT),
HumanMessage(content=question)
]
else:
messages=[
HumanMessage(content=question)]
result = graph.invoke({
"messages":messages},
config=config
)
return result["messages"][-1].content
def main():
print("===========================================")
print("CoOMPANY AI ASSISTANT")
print("===========================================")
print("\nType 'exist' to quit.")
thread_id = input("\nEnter conversation ID:")
while True:
question = input("\nYou: ")
if question.lower() in [
"exit",
"quit"
]:
break
try:
answer = chat(question,thread_id)
print(f"\nAI: {answer}")
except Exception as e:
print(f"\nError: {e}")
if __name__ == "__main__":
main()
import streamlit as st
from langchain_core.messages import (
HumanMessage,
AIMessage,
SystemMessage
)
from graph import graph
# Page configuration
st.set_page_config(
page_title="Company AI Assistant",
page_icon="🤖",
layout="centered"
)
# System prompt
SYSTEM_PROMPT = """
You are an intelligent company assistant.
You answer questions using company documents
and available tools.
Rules:
1. Use company_document_search whenever
the user asks about company-specific
information.
2. Never invent company policies,
benefits, rules, or procedures.
3. Use the calculator when mathematical
calculations are required.
4. When company documents are used,
mention the source document and page.
5. If the company documents do not contain
the requested information, clearly say
that you could not find it.
6. Use previous conversation context to
understand follow-up questions.
7. Give concise and clear answers.
"""
# Sidebar
st.sidebar.title("⚙️ Settings")
thread_id = st.sidebar.text_input(
"Conversation ID",
value="streamlit-user"
)
st.sidebar.markdown(
"""
### About
This application demonstrates:
- RAG
- AI Agents
- LangGraph
- FAISS
- Query rewriting
- Reranking
- Conversation memory
- Tool calling
"""
)
# Main UI
st.title("🏢 Company AI Assistant")
st.write(
"Ask questions about company policies, "
"benefits, leave, IT rules, and more."
)
# Chat history in Streamlit
if "chat_history" not in st.session_state:
st.session_state.chat_history = []
# Display previous messages
for message in st.session_state.chat_history:
with st.chat_message(
message["role"]
):
st.markdown(
message["content"]
)
# User input
question = st.chat_input(
"Ask something about the company..."
)
if question:
# Display user question
with st.chat_message("user"):
st.markdown(question)
st.session_state.chat_history.append(
{
"role": "user",
"content": question
}
)
# Build messages
config = {
"configurable": {
"thread_id": thread_id
}
}
# Check whether this thread
# already has state
state = graph.get_state(
config
)
if not state.values:
messages = [
SystemMessage(
content=SYSTEM_PROMPT
),
HumanMessage(
content=question
)
]
else:
messages = [
HumanMessage(
content=question
)
]
# Call LangGraph
with st.chat_message("assistant"):
with st.spinner(
"Thinking..."
):
try:
result = graph.invoke(
{
"messages": messages
},
config=config
)
answer = result[
"messages"
][-1].content
st.markdown(answer)
# Save answer
st.session_state.chat_history.append(
{
"role": "assistant",
"content": answer
}
)
except Exception as e:
st.error(
f"Something went wrong: {e}"
)
Чэк-ліст аператыўнай працы
Для чэк-ліста аператыўнай працы неабходна перад змянай коду адзначыць вхідныя даны, адпаведальнага за кожны крок і крэтыярыя завершэння. Аператары должны магчымае запускіць крок з вядомай точкі контролю, не падозрываючы прыхованы стан.
Неабходна аддзекаваць як шлях успеху, так і шлях вярнення. Перапрыбуткі, людзкія контрольныя пункты і обработка некорэктных паведамленняў ёсць часткай продукту, а не яго пазнейшай дапрацоўкі.
Наводзіць трэба тые часткі тэксту, якія фактычна служылі падставай для адпаведнай адпаведзі. Без цых цытатаў аператары не можуць разлічыць галюцынацію ад прасоў у індэксах.
Пауза пасля дорогіх крокаў. Сістэма адновлення не должна занова нараховваць плата за той самы вызыв LLM, калі аператар прабуе зноў запрацаваць пазнейшы вузел.
Фіксуйце версіі залежнасцей і запішыце хэш адпраўленага зображэння, якое вікорыстоўвалася для дэманстрацыі. Возможнасць павторнага стварэння сітуацыі лепш за традыцыйныя методы.
Запішыце час выканання задач і вартасьць токенав або запытанняў разам з функцыйнальнымі рэзультатамі. Ведаўка пра вартасьці з самага пачатку запобегае неспакою, калі працэс пераходзіць з дэманстрацыі ў спакульнаныя сераўеры.
Перш чым пераводзіць всю структуру, заморозьце версіі, зафіксуйце «золаты» транскрыпты для крытычных частак і паўтарна пераканайцеся ў кроках для вярнэння да пачатковага стану. У спакульных сераўерах неабходны ліміты на колькасьць запытанняў, пераказкі прав на выкарыстоўванне рэсурсаў і чысткі власнік для змены секрэтных даных. Лепш надзейнае, але простае рашэння, чым крэатывныя, але разовыя дэманстрацыі.
Прымечанне для 656e1cd26886: не кладзіце ключы прадаўца ў рэпазітарый, задаць ліміт на колькасьць токенав за сесыю і зберагаць транскрыпты разам з фіксатрамі для ацэнкі, каб пазнейшыя замены мадэляў заставаліся пораўнаннымі.
Для пункта 0 апдырэння неабяжна практычна частка: перад змінайом код неабяжна ваказаць параметры вводу, адпаведнага власніка крока і крытэрыя завершэння. Аператары должны магчымаць перзапуск крока з вядомай точкі контролю, не падозрываючы схованы стан. Конфігурацыю трэба знаходзіць за межама коду прыемленае. Файлы серавэра, хранільнікі секрэтных дадзеных і флагі функцый належыць у аднам месца, якое аператары можаць пераглядаць, не чытаяўшы весь код.
Дэталі апдырэння 0/780: памерыце час выканання, класія ошыбак і колькасць вікорыстоўваных токенаў для гэтага пункта, а потым вынікніце рашэнне пра тое, чы хацяць застаўіць змяну, стварыўшы фіксаваны набор пытанняў, а не спакойнае адгукванне.
Калі працуеце над пунктом 1 адзынківання, спачатку запісайце умовы: неабяжлівыя даны, сігнал успеху і тое, што выходзіць у разе частковага неяксаменства. Такі спіс контроля дапамагае залічыць пазнейшыя змены ў кодзе адкрыта і прозрачна. Валіце маленькія, тэставаныя елементы замест большых скрыптов. Калі якісь крок не выйшае, неяксаменства должны вказываць на адну конкрэтную адпаведальнасць, а не на заплутаны ланцюг задач.
Дзялянка адзынківання 1/780: замерайце час выканання, класы каштоўкаў і вітрату токенаў для гэтага пункту, а потым вырашайце, чы робіць змены на адной основе фіксаванага набора пытанняў, а не на адной лячбе.
Пункт 2 адзынківання будзе працаваць наяўней, калі яго спрыймаць як параметр, які можна замерыць. Запісайце адну ідэальную версію, адзін прыклад неяксаменства і запіс пра адворачэнне змены, перш чым расшырваць сферу ўплыву. Запісвайце часы выканання і вартасць токенаў або запыткаў разам з функцыйнальнымі рэзултатамі. Відразы вартасцей з самага пачатку запобегае неспакойным рашчыткам, калі працэс пераходзіць з дэмаверсіі ў спяльныя асяроддзя.
Дзеянне паўжчання 2/780: звярніце увагу на час выканання, класыя ошыбак і колькасць токенаў, якія былі выкарыстаны для гэтага запісу, а пасля, на аднойчынай базе фіксаванага набору пытанняў, а не на асобістых спазырэннях, выявіце, чы хацяце застаўіць змены.
Для запісу паўжчання 3 абявіце параметры вхідных дадзенняў, адпаведальнага за выкананне крока і крытэрыяы завершэння працы перад змінай коду. Аператары должны магчымае перадзвігнуць крок з вядомага пункта контролю, не спрабоўваючы здагадвацца пра схованы стан. Запісуйце аднойчынна шлях успеху і шлях вярнення. Прабыткі, людзкія перакрыцця і апрацоўка неканальных паведамленняў ёсць часткай продукту, а не чымсь, што дадаецца пазней.
Дзеянне паўжчання 3/780: звярніце увагу на час выканання, класыя ошыбак і колькасць токенаў, якія былі выкарыстаны для гэтага запісу, а пасля, на аднойчынай базе фіксаванага набору пытанняў, а не на асобістых спазырэннях, выявіце, чы хацяце застаўіць змены.
Калі працуеце над пунктом 4 адзынківання, спачатку запісайце контракт: неабяжлівыя вхідныя даны, сігнал успеху і тое, што выходзіць у разе частковага нявыпання. Такі спіс пераконвае ў тым, што пазнейшыя змены коду будуць чыстымі. Спрэтывайце да гэтага этапу як да контракту межа вхіднымі данымі і перакананымі выходнымі рэзультатамі. Даўце назвы артыфактам, задаце крытэрыя успеху і адмовіцеся ад тых частковых выпанняў, якія залішаюцца непазначанымі.
Дзялённе 4/780 адзынківання: змерьце час выкарыстоўвання, класыя ошыбакі і колькасць токенав, якія былі выкарыстаны для гэтага пункту, а потым вярніцеся да рашэння, чы хацеце застаўіць змену, адпаведна фіксованаму набору пытанняў, а не толькі на адной прымітцы.
Пункт 5 адзынківання будзе працаваць наяўней, калі яго спрэтываць як меравальную плошчу. Запісайце адну ідэальную транскрыпцыю, адзін прыклад нявыпання і прымітку па адкатаванні, перш чым расшырваць сферу дзеяння. Зберагайце настройкі парадульна ад коду прыемлівача. Файлы сяродавішча, хранільнікі секрэтных даных і флагі функцый должны знаходзіцца ў аднам месцы, якое аператары можаць пераглядаць без неабяжлівага чытання всей структуры.
Дзеянне паўжчання 5/780: звярніце увагу на час выканання, клас памылакі і колькасць выкорыстоўваных токенаў для гэтага запісу, а пасля, на аднойчынай базе фіксаванага набора пытанняў, а не на асобістых спазыраннях, выявіце, чы хацяце застаўіць змены.
Для дзеяння паўжчання 6 адначасна з змянай коду задаце вхідныя даны, адпаведальнага за крок і крытэрыя завершэння. Аперацыі должны магчымае перадзванаць крок з вядомай точкі контролю, не падозрываючы прыхованы стан. Валіце маленькія, тэставальныя елементы працы над кодам замест большых, заплутаных скрыптав. Калі крок не выконваецца, прычына неудачы должна вказываць на адну конкрэтную адпаведальнасць, а не на заплутаны ланцужок задач.
Дзеянне паўжчання 6/780: звярніце увагу на час выканання, клас памылакі і колькасць выкорыстоўваных токенаў для гэтага запісу, а пасля, на аднойчынай базе фіксаванага набора пытанняў, а не на асобістых спазыраннях, выявіце, чы хацяце застаўіць змены.
Калі працуеце над пунктом 7 адзынкіравання, спачатку запісайце умовы: неабяжлівыя даны, сігнал успеху і тое, што выходзіць пад частковыя неудачы. Такі спіс дапамагае заліцварваць будучыя змены коду. Запісуйце час выканання і вартасьць токена або запытку пад функцыйнальнымі рэзултатамі. Відразы вартасцей з самага пачатку запобегае неспакоўным рахункам, калі працэс пераходзіць з дэмаверсіі ў спяльныя асяроддзя.
Дзеянне адзынкіравання 7/780: замерайце час выканання, класію паканаў і вітраты токена для гэтага пункту, а потым выберайце, чы робіць змену на аднойчынных крэтарыях, а не на аснове індывідуальных спазыроў.
Пункт адзынкіравання 8 будзе работаць наяўней, калі яго спрыймаць як меравальную плошчу. Запісайце адну ідэальную транскрыпцыю, адзін кейс пакана і прыказку па абратанні рэшэнняя перад расшырэннем масштабаў. Дакументавайце як успешны, так і вярнучы паты. Перапрыбуткі, людзкія контралі і обработка неканальных паканаў ёсць часткай продукту, а не чымось, што дадаецца пазней.
Дзеянне паўжасткі 8/780: звярніце увагу на час выканання, клас памылак і колькасць токенаў, выкорыстаных для гэтай змяны, а пасля, на аднойчынку з фіксаваным наборам пытанняў, а не на асобістых спазыраннях, выявіце, чы рашаце застаўіць гэту змяну.
Для дзеяння паўжасткі 9 адначасна з змянай коду неабходна парадактаваць параметры, адначасова вялікія працавальнікі і крэтырыя завершэння. Аперацыёныя працавальнікі павінны магчымае перадзвяжаць гэты крок з вядомага пункта контролю, не прымусваныя здагадвацца пра схованы стан. Штодзе гэты этап трэба спрыяць як кантракт між параметрамі і перакананымі рэзультатамі. Назвіце всі неабходныя элементы, парадактаваць крэтырыі успеху і не падтрымвайце частковае завершэння без паведамлення.
Дзеянне паўжасткі 9/780: звярніце увагу на час выканання, клас памылак і колькасць токенаў, выкорыстаных для гэтай змяны, а пасля, на аднойчынку з фіксаваным наборам пытанняў, а не на асобістых спазыраннях, выявіце, чы рашаце застаўіць гэту змяну.