Архітектура рішень, заснованих на LLM, для великих ентерпрайзів
Усі статті, обговорення, новини про AI — в одному місці. Підписуйтеся на DOU | AI!
Привіт! Мене звати Дмитро, я Senior Engineering Manager у компанії GlobalLogic. Загалом моя щоденна діяльність — це побудова рішень, заснованих на LLMs (Large Language Models), для великих ентерпрайзів. І в цьому матеріалі я розповім вам про архітектуру таких рішень.
Почнемо з базового. Сфера Artificial Intelligence, де великі нейронні мережі формують картинки або відповіді на будь-які питання, є похідною від Machine Learning. У Machine Learning є структури даних та алгоритми, котрі їх обчислюють і формують інші структури для обчислення та вирішення двох базових завдань — класифікування та регресії. Тобто все те, що ми бачимо в тому ж ChatGPT, є результатом роботи великої кількості machine learning algorithms.
Більшість людей, які щось чули про AI, вважають, що для використання якої-небудь кастомної функціональності треба навчати нові моделі на релевантній вибірці даних. Це не завжди так. Дійсно, якщо нам потрібні швидкі обчислення під конкретний кейс (наприклад, класифікувати, чи цей конкретний запит до сервера є fraud attempt), то краще буде навчити свою модель на вибірці спеціально підготовлених даних. Але якщо завдання полягає в роботі зі знаннями, пошуку семантичних ознак одного контексту в іншому, формуванні нового контексту з бази знань і навіть тій же операції з fraud detection, але не у mission critical середовищі — тут домінують
Уявіть собі, що ви маєте команду з роботів, які розуміють натуральну людську мову та можуть методами статистичних обчислень формувати відповідь на будь-яке питання з тієї інформації (контексту), до якої мають доступ. Роботи в цій команді можуть бути дуже розумними, багато знати та володіти аналітичними навичками (GPT-4, GPT-4o, Llama 70B). Або можуть просто виконувати однотипні робочі завдання, щодо яких вони мають дуже чітку інструкцію та знання (Llama 7B та ін.). Загалом побудова застосунку на базі LLM нагадує класичну діяльність менеджера, який будує процес: треба пояснити людям
Це коротка стаття, яка не претендує на академічне дослідження. На прикладах своїх пет-проєктів я пояснюю базові рішення для
Архітектура RAG та GraphRAG
Коли ми говоримо про RAG (Retrieval-Augmented Generation) або Graph + RAG, то загальна архітектура виглядає так:
Тут може бути або тільки семантичний пошук по векторизованих ембедингах, або комбінація векторного пошуку й пошуку по створеному knowledge graph. Семантичний пошук дає основний контекст, який має дати моделі знання для того, щоб відповісти на запитання в оригінальному промпті. Але зазвичай деякі моделі дуже генералізують фінальну відповідь, тому що не звертають уваги на певні подробиці в тексті з вирішальним значенням. Для того, щоб додати в контекст більше конкретних даних як безпосередній факт, використовують knowledge graph. Він надає структуровані дані, що виглядають так:
Project Management — INVOLVES -> Team
Project Management — AIMS_TO_ACHIEVE -> Project Goals
Project Management — WITHIN -> Constraints
Project Management — AIMS_TO_COMPLY_WITH -> Client’S Objectives
Project Management — DEVELOPED_FROM -> Civil Construction
Project Management — DEVELOPED_FROM -> Engineering
Project Management — DEVELOPED_FROM -> Heavy Defense Activity
Project Management — ARISING_FROM -> Management Discipline
Project Management — ARISING_FROM -> Engineering Model
Project Management — USES -> Project Management Triangle
Project Management — HAS_CONSTRAINT -> Time
Project Management — HAS_CONSTRAINT -> Finance
Project Management — HAS_CONSTRAINT -> Human Resources
Engineering Fields — CONTRIBUTED_TO -> Project Management
Project Team — PARTICIPATES_IN -> Project Management
Modern Project Management — INCLUDES -> Work Breakdown Structure
Modern Project Management — INCLUDES -> Resource Allocation
Scientific Management — FORERUNNER -> Modern Project Management
1950S — BEGINNING -> Modern Project Management
Такі структуровані дані додаються до загального промпту як частина контексту, якій може передувати частина промпту на кшталт «Pay extra attention to the entities and connections between them in the structured data context below and use any relevant structured data in your reply to add more specific details matching the {question}».
RAG використовують як компонент у багатьох сценаріях, де потрібно працювати зі знаннями організації або отримувати дані з репортів кількох департаментів, перетворювати їх на нові репорти чи трансформувати в щось інше.
Покращення результатів векторного пошуку
Для покращення результатів видачі у векторному пошуку та генерації відповіді можна використати MedPrompt-фреймворк, який реалізує такі прийоми:
- In-Context Learning (ICL) / kNN few-shot.
- Chain-of-Thought (CoT).
- Ensemble choice shuffle.
kNN few-shot: LLM краще розуміють, що від них вимагають, якщо разом з основним промптом надати декілька прикладів. kNN few-shot дуже схожий на RAG, та є основна відмінність: у RAG ми векторизуємо «сиру» базу знань (добірку подрібнених документів), а у few-shot kNN — пари «запит — відповідь». Через методи MedPrompt отримуємо «запит — розмірковування (CoT) — відповідь» (розмірковування та відповідь можуть бути як згенеровані LLM, так і прописані мануально експертом). Тобто few-shot можна використовувати як статично (написавши руками в промпті), так і динамічно (в режимі kNN, коли за запитом юзера з векторної БД дістаються схожі приклади запитів з правильними відповідями). Це значно покращує якість відповідей.
Ось можливий приклад промпту:
few_shots_prompt_with_context = Template( """{% for item in examples %}## Question {{ item.question }} ## Context {{ item.answer }} {% endfor %}## Question {{ question }} ## Context """ ) google_chain_of_thoughts = { "prompt_name": "google_chain_of_thoughts", "response_type": "MC", "prompt": few_shots_prompt, "examples": [ { "question": """A 6-year-old girl is brought to the emergency department because of right elbow swelling and pain 30 minutes after falling onto her outstretched right arm. She has been unable to move her right elbow since the fall. Physical examination shows bruising, swelling, and tenderness of the right elbow; range of motion is limited by pain. An x-ray of the right arm shows a supracondylar fracture of the humerus with anterior displacement of the proximal fragment. Further evaluation is most likely to show which of the following findings? A. Absent distal radial pulse B. Radial deviation of the wrist C. Inability to abduct shoulder D. Inability to flex the elbow """, "answer": """Explanation: Let's solve this step-by-step, referring to authoritative sources as needed. A supracondylar fracture of the humerus with anterior displacement can potentially injure the brachial artery which runs anterior to the humerus and is responsible for the main blood supply to the forearm, wrist, and hand. This injury can cause an absent distal radial pulse due to decreased blood flow. Answer: [A].""", }, { "question": """Two weeks after undergoing low anterior resection for rectal cancer, a 52-year-old man comes to the physician because of swelling in both feet. He has not had any fever, chills, or shortness of breath. His temperature is 36 °C (96.8 °F) and pulse is 88/min. Physical examination shows a normal thyroid and no jugular venous distention. Examination of the lower extremities shows bilateral non-pitting edema that extends from the feet to the lower thigh, with deep flexion creases. His skin is warm and dry, and there is no erythema or rash. Microscopic examination of the interstitial space in this patient's lower extremities would be most likely to show the presence of which of the following? A. Acellular, protein-poor fluid B. Lymphocytic, hemosiderin-rich fluid C. Lipid-rich, protein-rich fluid D. Protein-rich, glycosaminoglycan-rich fluid """, "answer": """Explanation: Let's solve this step-by-step, referring to authoritative sources as needed. This patient's symptoms of bilateral non-pitting edema following a surgical procedure involving removal of regional lymph nodes suggest the diagnosis of lymphedema. Lymphedema is a condition characterized by localized fluid retention and tissue swelling caused by a compromised lymphatic system. The fluid accumulated in lymphedema is not merely protein-rich, but also lipid-rich. This occurs due to the blockage of lymphatic vessels which normally serve to drain both lipids and proteins. The interstitial fluid in lymphedema contains a high concentration of proteins, lipids, cells, and cellular debris. Answer: [C].""", }, { "question": """A 34-year-old gravida 2, para 1 woman at 37+6 weeks of gestation presents for elective cesarean delivery. She says she has been having increased fatigue over the past few weeks. Past medical history includes gestational hypertension for which she has been taking an antihypertensive drug twice daily since week 24. Her vital signs include: temperature 36.7°C (98.0°F), blood pressure 120/75 mm Hg, pulse 127/min. Physical examination reveals generalized pallor. Her laboratory results reveal microcytic, hypochromic anemia with anisocytosis, hemoglobin of 9 g/dL, a differential with 14% lymphocytes, an ESR of 22 mm/hr, and a reticulocyte production index of 3.1. A direct antiglobulin test is positive. LFTs, creatinine, ferritin level, vitamin B12 level, coagulation studies, and urinalysis are normal. Which of the following is the most likely diagnosis in this patient? A. Preeclampsia B. Hereditary spherocytosis C. HELLP syndrome D. Drug-induced immune hemolytic reaction """, "answer": """Explanation: This patient's findings of pallor, tachycardia, microcytic hypochromic anemia with anisocytosis, and a positive direct antiglobulin test (Coombs test) in the setting of an antihypertensive medication use are suggestive of drug-induced immune hemolytic anemia. This condition occurs when drugs (eg, ceftriaxone, penicillin, quinidine, methyldopa) bind to the red blood cell membrane, causing production of anti-drug antibodies that result in hemolysis. The direct antiglobulin test is positive due to the presence of IgG on the red blood cell membrane. Given the recent medication initiation, cessation of the medication is necessary to stop the hemolysis. Answer: [D].""", }, { "question": """You are counseling a pregnant woman who plans to breast-feed exclusively regarding her newborn's nutritional requirements. The child was born at home and the mother only plans for her newborn to receive vaccinations but no other routine medical care. Which vitamins should be given to the newborn? A. Folic acid B. Vitamin K C. Vitamin D D. Vitamin K and Vitamin D """, "answer": """Explanation: Exclusively breastfed infants are at risk of vitamin D deficiency because breast milk is low in vitamin D, and homebound infants may not get adequate sunlight exposure. In addition, vitamin K deficiency can occur in newborns due to inadequate transplacental transfer, low levels in breast milk, and sterile newborn gut (vitamin K is synthesized by gut bacteria). Thus, infants should receive prophylactic Vitamin K at birth to prevent Vitamin K deficiency bleeding, and Vitamin D supplementation should be started within the first few days of life and continue throughout infancy for those who are exclusively or partially breastfed. Answer: [D].""", }, { "question": """An investigator is studying nutritional deficiencies in humans. A group of healthy volunteers are started on a diet deficient in pantothenic acid. After 4 weeks, several of the volunteers develop irritability, abdominal cramps, and burning paresthesias of their feet. These symptoms are fully reversed after reintroduction of pantothenic acid to their diet. The function of which of the following enzymes was most likely impaired in the volunteers during the study? A. Methionine synthase B. Alpha-ketoglutarate dehydrogenase C. Glutathione reductase D. Dopamine beta-hydroxylase """, "answer": """Explanation: Pantothenic acid, also known as vitamin B5, is a component of coenzyme A (CoA), which is necessary for the metabolism of carbohydrates, amino acids, and lipids. CoA is also involved in the citric acid cycle (or Krebs cycle), where it forms a complex with acetyl groups (acetyl-CoA) to be oxidized for energy production. Alpha-ketoglutarate dehydrogenase is a key enzyme in the citric acid cycle and requires CoA for its function. Thus, a deficiency in pantothenic acid impairs the function of alpha-ketoglutarate dehydrogenase and disrupts the citric acid cycle, leading to the symptoms observed in the volunteers. Answer: [B].""", }, ], } # The example is generated by # Let's think step by step. Then, write your answer by [A], [B], [C] or [D].
Chain-of-thought (CoT): це ланцюжок розмірковувань, де ми кажемо моделі подумати перед вибором фінальної відповіді. Наприклад: «Before giving a response, describe your observations in 3 sentences with grounding the approach we should follow in <draft></draft> tags». Є багато методів, як зробити CoT. Але основне, що це дозволяє моделі «порефлексувати» і зробити драфти відповідей або обрати, на що треба звернути увагу, перед тим як дати фінальну відповідь. Це також поліпшує результат.
Ensemble with choice shuffle: у цьому випадку на виході очікується варіант відповіді, обраний із заздалегідь сформованого списку. На практиці порядок варіантів, з яких треба обрати, зсуває вибір варіантів у разі багаторазового перезапуску. Щоб це обійти, ми перемішуємо варіанти, наприклад, п’ять разів (тобто створюємо п’ять різних варіацій ордерінгу відповідей) та обираємо найпопулярніший (що потребує п’яти звертань до LLM замість одного). На виході обрані п’ять варіантів порівнюються за типом averaging або majority vote.
Створення ланцюжка промптів
У багатьох завданнях недостатньо просто зробити запит на отримання якоїсь прийнятної відповіді від LLM. Якщо потрібно, наприклад, розв’язати проблему, де треба витягнути з бази знань необхідні дані, потім виокремити з них суто фінансові дані й відтак зробити з них json для додавання в запит до API фінансового інструменту — у такому випадку створення промпту типу «знайди всі дані щодо виплат працівникам за 2023 рік, просумуй їх за місяцями і створи з них json, як вказано в прикладі» не буде давати прийнятного результату.
Зазвичай процес імплементації такого юзкейсу потребує декількох етапів:
- Екстракція даних.
- Перевірка даних.
- Перевірка математичних операцій.
- Перевірка сформованого json.
І тоді як перевірити дані й сформований json ми можемо, якщо створимо ланцюжок розмірковувань з прикладу вище й витратимо на це один запит до LLM, то для перевірки математичних операцій можуть знадобитися обчислення конвенційними засобами мови програмування і додавання їх до ще одного запиту до LLM разом з оригінальною відповіддю. Навіть GPT-4o не дуже добре працює з кейсами, де потрібно виконати багато операцій складання або віднімання (із множенням справляється без помилок), і зазвичай галюцинує. Тобто операція типу 768+890+577+342+869+234 може дати відповідь 3990 замість 3680. Щось на кшталт, як ми пригадаємо в умі, округляючи до сотні. У цьому випадку, щоб змусити LLM виправити себе та додати правильні результати обчислень в усі місця оригінальної відповіді, треба:
- Отримати першу відповідь.
- Перевірити наявність математичних операцій у ній.
- Виокремити операції та обчислити їх звичайними засобами.
- Додати проведені обчислення до оригінальної відповіді.
- Сформувати self-check промпт і відправити до LLM.
Код може виглядати ось так:
# -------------------------- Mathematical calculations check ------------- def extract_expressions(response: str) -> List[str]: # Adjust the regex to match the format in the response, including all arithmetic operations pattern = re.compile(r'(\d+\.?\d*\s*[\+\*/\-]\s*\d+\.?\d*(?:\s*[\+\*/\-]\s*\d+\.?\d*)*)\s*=\s*([\d,\.]+)') return pattern.findall(response) def perform_expression_calculation(expression: str) -> float: # Remove commas for proper evaluation expression = expression.replace(',', '') try: return eval(expression) except Exception as e: print(f"Error evaluating expression {expression}: {e}") return None def validate_calculations(response: str) -> str: expressions = extract_expressions(response) if not expressions: print("No calculations detected") return response # No calculation to validate corrected_response = response for original_expr, original_result in expressions: validated_result = perform_expression_calculation(original_expr) if validated_result is not None: corrected_expr = f"{original_expr.strip()} = {validated_result:,.2f}" corrected_response = corrected_response.replace( f"{original_expr} = {original_result}", f"{original_expr} = {original_result}, - proper result is {corrected_expr}" ) return corrected_response # -------------------------- Define the RAG chain ------------------------ # initial chain template = """Answer the question based only on the following context: {context} If there are mathematical calculations in the response, make sure to provide them in an explicit way. Example: 1+2+3+4+5 = 15 (4*6)/6=4 Question: {question} """ prompt = ChatPromptTemplate.from_template(template) chain = ( RunnableParallel( { "context": retriever, "question": RunnablePassthrough(), } ) | prompt | llm | StrOutputParser() ) # final validation chain def get_initial_response(question): return chain.invoke(question) def return_initial_response(initial_response): return initial_response def get_response_validation(initial_response): return validate_calculations(initial_response) def process_question(question: str) -> str: try: initial_response = get_initial_response(question) if extract_expressions(initial_response): print("Need to validate calculations") template_validate = """ The following response might contain calculation or semantic errors: {response_for_validation} The proper result of calculations should be {validated_data}. Double-check and correct any errors in the calculations or semantics based on the initial request. Reply with the original message format, correct all calculations and results of calculations in all places of the message including tables, written expressions, formulas etc. """ prompt_validate = ChatPromptTemplate.from_template(template_validate) chain_validate = ( RunnableParallel( { "response_for_validation": lambda _: initial_response, "validated_data": lambda _: get_response_validation(initial_response), "question": RunnablePassthrough(), } ) | prompt_validate | llm | StrOutputParser() ) final_response = chain_validate.invoke(question) print(f"Final response after validation: {final_response}") return final_response else: return initial_response except Exception as e: logger.error(f"Error processing question: {e}") return "An error occurred while processing your request."
Такий підхід гарантує, що помилки під час математичних операцій будуть виправлені й LLM, відповідно, замінить їх на правильні у фінальній відповіді.
На завершення
Наш матеріал, зосереджений на архітектурі
Для тих, хто хоче поглибити свої практичні навички в
- Дуже круті безплатні мінікурси від OpenAI and Andrew Ng.
- Трохи промпт-інжинірингу для розробників.
- Про параметри LLM, які впливають на формування відповіді.
12 коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарів