[{"data":1,"prerenderedAt":1589},["ShallowReactive",2],{"post-document:\u002Fbackend\u002Fspring_ai\u002F2026\u002F08\u002F19\u002F요약\u002F":3},{"id":4,"title":5,"body":6,"categories":1575,"date":1578,"description":16,"extension":1579,"image":1580,"key_concepts":1580,"last_modified_at":1580,"legacyPath":1581,"meta":1582,"navigation":452,"part":1580,"path":1584,"published":452,"robots":1580,"seo":1585,"series":1580,"stem":1586,"strengths":1580,"summary":1580,"tags":1587,"tradeoffs":1580,"__hash__":1588},"posts\u002Fposts\u002FBackend\u002FSPRING_AI \u002F요약.md","이번 강의는 무엇을 노리고 있을까?",{"type":7,"value":8,"toc":1561},"minimark",[9,13,17,23,34,39,45,50,53,57,62,65,68,90,96,101,105,108,176,181,184,189,192,196,201,239,242,280,283,306,309,312,316,319,322,328,331,339,342,346,349,394,398,404,407,410,413,419,422,426,460,463,515,520,523,546,563,566,570,573,594,597,620,623,626,643,647,650,656,659,662,668,671,675,678,681,692,695,706,709,712,717,720,723,746,749,753,756,759,765,768,774,777,780,797,802,805,810,880,885,888,891,935,938,942,947,950,953,956,1000,1003,1026,1030,1033,1039,1042,1045,1065,1068,1071,1075,1078,1084,1087,1090,1094,1099,1102,1125,1128,1131,1137,1140,1144,1147,1150,1196,1199,1202,1205,1209,1212,1244,1247,1253,1256,1260,1337,1341,1344,1453,1457,1460,1463,1467,1471,1477,1491,1495,1512,1515,1526,1530,1536,1550,1554,1557],[10,11,5],"h1",{"id":12},"이번-강의는-무엇을-노리고-있을까",[14,15,16],"p",{},"Spring Boot 계층 구조 위에 AI를 얹고, 프롬프트로 출력을 제어하고, RAG로 근거를 붙이고, Tool로 실제 행동을 수행한 뒤에, 보안과 메모리, 운영지표 관리해보자",[14,18,19],{},[20,21,22],"span",{},"문서 사전 처리",[24,25,31],"pre",{"className":26,"code":28,"language":29,"meta":30},[27],"language-text","문서\n\n-> DocumentReader\n-> Splitter\n-> Chunk\n-> EmbeddingModel\n-> VectorStore\n","text","",[32,33,28],"code",{"__ignoreMap":30},[14,35,36],{},[20,37,38],{},"사용자 요청 처리",[24,40,43],{"className":41,"code":42,"language":29,"meta":30},[27],"사용자\n-> Controller\n-> Service\n-> ChatClient\n-> Advisor 체인\n    -> Safety\n    -> Memory\n    -> RAG\n    -> Audit\n    -> Token Meter\n-> ChatModel\n    -> 문서가 필요하면 RAG\n    -> 실시간 행동이 필요하면 Tool\n-> 구조화된 DTO\n-> 사용자 응답\n",[32,44,42],{"__ignoreMap":30},[14,46,47],{},[20,48,49],{},"운영",[14,51,52],{},"토큰 측정\n지연 시간 측정\n오류율 측정\n감사 로그\n폴백 모델",[10,54,56],{"id":55},"공부하기-전에-알아놓을-개념","공부하기 전에 알아놓을 개념",[58,59,61],"h2",{"id":60},"_1-llm은-기억하거나-시실을-조회하거나-프로그램이-아니다","1. LLM은 기억하거나 시실을 조회하거나 프로그램이 아니다.",[14,63,64],{},"LLM은 입력을 읽고 다음 토큰을 확률적으로 생성하는 모델",[14,66,67],{},"따라서 다음 특성이 생김",[69,70,71,75,78,81,84,87],"ul",{},[72,73,74],"li",{},"같은 질문에도 답이 조금 달라짐",[72,76,77],{},"모르는 내용을 그럴듯하게 만들 수 있다.",[72,79,80],{},"대화 내요을 스스로 기억하지 않는다",[72,82,83],{},"이전 대화를 기억시키려면 매번 대화 이력을 다시 넣어야 한다",[72,85,86],{},"입력과 출력의 길이, 비용, 속도는 토큰을 기준으로 결정된다.",[72,88,89],{},"컨텍스트 윈도우를 넘으면 오래된 내용이 잘리거나 사용할 수 없게 된다.",[91,92,93],"blockquote",{},[14,94,95],{},"Memory는 진짜 기억이 아니라, 이전 대화를 저장했다가 다시 프롬프트에 넣는 기능",[91,97,98],{},[14,99,100],{},"RAG는 진짜 학습이 아니라, 외부 문서를 찾아 프롬프트에 넣는 기능",[58,102,104],{"id":103},"_2-ai도-기존-spring-boot-계층-구조-안에-넣어야-한다","2. AI도 기존 Spring Boot 계층 구조 안에 넣어야 한다.",[14,106,107],{},"기본 역할은 다음과 같다.",[109,110,111,124],"table",{},[112,113,114],"thead",{},[115,116,117,121],"tr",{},[118,119,120],"th",{},"계층",[118,122,123],{},"책임",[125,126,127,136,144,152,160,168],"tbody",{},[115,128,129,133],{},[130,131,132],"td",{},"Controller",[130,134,135],{},"HTTP 요청 수신, 입력 검증, 응답 반환",[115,137,138,141],{},[130,139,140],{},"Service",[130,142,143],{},"업무 흐름, 정책 판단, AI 호출 조합",[115,145,146,149],{},[130,147,148],{},"Repository 또는 Mapper",[130,150,151],{},"DB와 외부 시스템 접근",[115,153,154,157],{},[130,155,156],{},"DTO",[130,158,159],{},"계층 간 요청과 응답 형식",[115,161,162,165],{},[130,163,164],{},"Config",[130,166,167],{},"ChatClient, Advisor, VectorStore 같은 빈 구성",[115,169,170,173],{},[130,171,172],{},"Advisor",[130,174,175],{},"로깅, 안전, 메모리, RAG 같은 공통 처리",[91,177,178],{},[14,179,180],{},"가장 중요한 규칙은 Controller가 ChatClient를 직접 호출하지 않는 것.",[14,182,183],{},"Controller는 AI가 어떤 모델을 사용하는지, 어떤 프롬프트를 사용하는지, RAG가 붙는지 알 필요가 없다. Controller는 Service만 호출하고,Service가 적절한 시점에 ChatClient를 호출해야 한다.",[91,185,186],{},[14,187,188],{},"모델이나 프롬프트가 바뀌어도 웹 계층이 영향을 받지 않도록 설계해야 한다.",[14,190,191],{},"자료 역시 AI 코드를 Config, Advisor, Service로 분산하고, Controller는 AI 모르게 하도록 정리해야 한다.",[10,193,195],{"id":194},"_2-spring-ai의-중심은-추상화","2 Spring AI의 중심은 추상화",[69,197,198],{},[72,199,200],{},"추상화? : 필요한 기능만 보여주는 것 (메서드로 구현한다.)",[109,202,203,213],{},[112,204,205],{},[115,206,207,210],{},[118,208,209],{},"추상화",[118,211,212],{},"역할",[125,214,215,223,231],{},[115,216,217,220],{},[130,218,219],{},"ChatModel",[130,221,222],{},"프롬프트를 받아 텍스트를 생성",[115,224,225,228],{},[130,226,227],{},"EmbeddingModel",[130,229,230],{},"텍스트를 의미 벡터로 변환",[115,232,233,236],{},[130,234,235],{},"VectorStore",[130,237,238],{},"벡터를 저장하고 유사한 문서를 검색",[14,240,241],{},"실제로도 ChatClient를 주로 사용한다.",[24,243,247],{"className":244,"code":245,"language":246,"meta":30,"style":30},"language-java shiki shiki-themes github-light github-dark","String answer = chatClient.prompt()\n    .system(\"너는 상담 도우미다.\")\n    .user(question)\n    .call()\n    .content();\n","java",[32,248,249,256,262,268,274],{"__ignoreMap":30},[20,250,253],{"class":251,"line":252},"line",1,[20,254,255],{},"String answer = chatClient.prompt()\n",[20,257,259],{"class":251,"line":258},2,[20,260,261],{},"    .system(\"너는 상담 도우미다.\")\n",[20,263,265],{"class":251,"line":264},3,[20,266,267],{},"    .user(question)\n",[20,269,271],{"class":251,"line":270},4,[20,272,273],{},"    .call()\n",[20,275,277],{"class":251,"line":276},5,[20,278,279],{},"    .content();\n",[14,281,282],{},"ChatClient는 단순한 모델 호출 이외에도 다음 기능을 제공",[69,284,285,288,291,294,297,300,303],{},[72,286,287],{},"시스템 프롬프트",[72,289,290],{},"호출 옵션",[72,292,293],{},"Advisor 연결",[72,295,296],{},"Tool 연결",[72,298,299],{},"구조화 출력",[72,301,302],{},"스트리밍",[72,304,305],{},"응답 메타데이터",[14,307,308],{},"세 추상화를 잘 결합하면 채팅, 검색, RAG, 문서 검색, 에이전트 기능으로 확장할 수 있다.",[14,310,311],{},"또한 애플리케이션 코드는 추상화에 의존하고, 공급자는 Gradle 의존성과 application.yml 설정으로 교체하도록 설계",[10,313,315],{"id":314},"_3-chatclient는-용도별로-나누는-것이-좋다","3. ChatClient는 용도별로 나누는 것이 좋다.",[14,317,318],{},"하나의 ChatClient로 요약, 분류, 창작, 상담을 모두 처리하면 설정이 충돌한다.",[14,320,321],{},"따라서 아래와 같이 나누는 것이 좋다.",[24,323,326],{"className":324,"code":325,"language":29,"meta":30},[27],"extractClient\n    목적 : 분류, 추출\n    temperature : 0\n    규칙 : 추측 금지, 없으면 null\nsupportClient\n    목적 : 상담\n    temperature : 0.2\n    규칙 : 친절한 문장\nideaClient\n    목적 : 아이디어 생성\n    temperature : 0.8\n    규칙 : 다양한 아이디어\n",[32,327,325],{"__ignoreMap":30},[14,329,330],{},"온도는 모델의 지식향을 조절하는 값이 아니락, 출력의 무작위성을 조절",[69,332,333,336],{},[72,334,335],{},"분류, 추출, 요약 : 낮은 온도",[72,337,338],{},"아이디어, 창작 : 높은 온도",[14,340,341],{},"또한 시스템 프롬프트, 옵션, Advisor는 호출할 때마다 적는 것이 아니라 ChatClient Bean의 기본값으로 구성하는 편이 일관성이 높다.",[10,343,345],{"id":344},"_4-프롬프트는-업무-지지서","4. 프롬프트는 업무 지지서",[14,347,348],{},"종흔 프롬프트의 기본 요소는 네 가지",[109,350,351,361],{},[112,352,353],{},[115,354,355,358],{},[118,356,357],{},"요소",[118,359,360],{},"질문",[125,362,363,370,378,386],{},[115,364,365,367],{},[130,366,212],{},[130,368,369],{},"누구의 관점에서 답하는가",[115,371,372,375],{},[130,373,374],{},"맥락",[130,376,377],{},"어떤 자료와 상황을 참고하는가",[115,379,380,383],{},[130,381,382],{},"지시",[130,384,385],{},"정확히 무엇을 해야 하는가",[115,387,388,391],{},[130,389,390],{},"형식",[130,392,393],{},"어떤 모양으로 출력해야 하는가",[14,395,396],{},[20,397,212],{},[24,399,402],{"className":400,"code":401,"language":29,"meta":30},[27],"너는 사내 고객 문의 분류기다.\n\n[맥락]\n문의 카테고리는 결제, 배송, 환불, 기타 네 가지다.\n\n[지시]\n사용자 문의를 가장 적절한 카테고리로 분류하라.\n확신이 없으면 기타로 분류하라.\n\n[형식]\n카테고리 이름 하나만 출력하라.\n",[32,403,401],{"__ignoreMap":30},[14,405,406],{},"-> 업무 지시는 이렇게 하는 게 좋음",[14,408,409],{},"말로 설명하기 어려운 경우에는",[14,411,412],{},"FewShot 예시를 넣도록",[24,414,417],{"className":415,"code":416,"language":29,"meta":30},[27],"입력: 카드 결제가 두 번 됐어요\n출력: 결제\n\n입력: 택배가 아직 안 왔어요\n출력: 배송\n",[32,418,416],{"__ignoreMap":30},[14,420,421],{},"프롬프트가 계속 길어진다면 지시를 계속 추가하기보다 작업을 Router, Chaining, Evaluator 단계로 나눌 수 있다. 자료는 정확도가 부족할 때 프롬프트를 계속 늘리지 말고 호출을 테스트 가능한 단계로 분리하라고 강조",[10,423,425],{"id":424},"_5-문자열보다는-구조화-출력을-사용해야-한다","5. 문자열보다는 구조화 출력을 사용해야 한다.",[24,427,429],{"className":244,"code":428,"language":246,"meta":30,"style":30},"String result = chatClient.prompt()\n    .user(question)\n    .call()\n    .content();\n\nString[] values = result.split(\",\");\n",[32,430,431,436,440,444,448,454],{"__ignoreMap":30},[20,432,433],{"class":251,"line":252},[20,434,435],{},"String result = chatClient.prompt()\n",[20,437,438],{"class":251,"line":258},[20,439,267],{},[20,441,442],{"class":251,"line":264},[20,443,273],{},[20,445,446],{"class":251,"line":270},[20,447,279],{},[20,449,450],{"class":251,"line":276},[20,451,453],{"emptyLinePlaceholder":452},true,"\n",[20,455,457],{"class":251,"line":456},6,[20,458,459],{},"String[] values = result.split(\",\");\n",[14,461,462],{},"모델이 쉼표를 빼거나 설명을 추가하면 깨질 수 있으므로 일단 형식으로 정리",[24,464,466],{"className":244,"code":465,"language":246,"meta":30,"style":30},"enum Category {\n    BILLING, DELIVERY, REFUND, ETC\n}\n\nrecord Ticket(\n    Category category,\n    int priority,\n    List\u003CString> keywords\n) {}\n",[32,467,468,473,478,483,487,492,497,503,509],{"__ignoreMap":30},[20,469,470],{"class":251,"line":252},[20,471,472],{},"enum Category {\n",[20,474,475],{"class":251,"line":258},[20,476,477],{},"    BILLING, DELIVERY, REFUND, ETC\n",[20,479,480],{"class":251,"line":264},[20,481,482],{},"}\n",[20,484,485],{"class":251,"line":270},[20,486,453],{"emptyLinePlaceholder":452},[20,488,489],{"class":251,"line":276},[20,490,491],{},"record Ticket(\n",[20,493,494],{"class":251,"line":456},[20,495,496],{},"    Category category,\n",[20,498,500],{"class":251,"line":499},7,[20,501,502],{},"    int priority,\n",[20,504,506],{"class":251,"line":505},8,[20,507,508],{},"    List\u003CString> keywords\n",[20,510,512],{"class":251,"line":511},9,[20,513,514],{},") {}\n",[69,516,517],{},[72,518,519],{},"여기서 알고 가기 : record = 여러 데이터를 하나로 묶어 전달하기 위한 간결한 불변 데이터 객체",[14,521,522],{},"그리고 객체로 받는다.",[24,524,526],{"className":244,"code":525,"language":246,"meta":30,"style":30},"Ticket ticket = chatClient.prompt()\n    .user(question)\n    .call()\n    .entity(Ticket.class);\n",[32,527,528,533,537,541],{"__ignoreMap":30},[20,529,530],{"class":251,"line":252},[20,531,532],{},"Ticket ticket = chatClient.prompt()\n",[20,534,535],{"class":251,"line":258},[20,536,267],{},[20,538,539],{"class":251,"line":264},[20,540,273],{},[20,542,543],{"class":251,"line":270},[20,544,545],{},"    .entity(Ticket.class);\n",[69,547,548,551,554,557,560],{},[72,549,550],{},"문자열 파싱 코드가 사라진다.",[72,552,553],{},"필드 타입이 보장된다.",[72,555,556],{},"enum으로 허용값을 제한할 수 있다.",[72,558,559],{},"목록과 중첩 객체를 그대로 받을 수 있다.",[72,561,562],{},"형식 위반을 조용히 통과시키지 않고 실패시킬 수 있다.",[14,564,565],{},"다만 모델 출력은 항상 성공하지 못하므로, 자료 온도 0, 형식 재요청, 안전한 기본값의 세 단계를 준비해야 함.",[10,567,569],{"id":568},"_6-스트리밍은-총시간보다-체감시간을-줄인다","6. 스트리밍은 총시간보다 체감시간을 줄인다",[14,571,572],{},"call()은 응답이 완성될 때까지 기다린 뒤 한 번에 반환한다.",[24,574,576],{"className":244,"code":575,"language":246,"meta":30,"style":30},"String answer = chatClient.prompt()\n    .user(question)\n    .call()\n    .content();\n",[32,577,578,582,586,590],{"__ignoreMap":30},[20,579,580],{"class":251,"line":252},[20,581,255],{},[20,583,584],{"class":251,"line":258},[20,585,267],{},[20,587,588],{"class":251,"line":264},[20,589,273],{},[20,591,592],{"class":251,"line":270},[20,593,279],{},[14,595,596],{},"stream()은 생성되는 조각을 바로 반환한다.",[24,598,600],{"className":244,"code":599,"language":246,"meta":30,"style":30},"Flux\u003CString> answer = chatClient.prompt()\n    .user(question)\n    .stream()\n    .content();\n",[32,601,602,607,611,616],{"__ignoreMap":30},[20,603,604],{"class":251,"line":252},[20,605,606],{},"Flux\u003CString> answer = chatClient.prompt()\n",[20,608,609],{"class":251,"line":258},[20,610,267],{},[20,612,613],{"class":251,"line":264},[20,614,615],{},"    .stream()\n",[20,617,618],{"class":251,"line":270},[20,619,279],{},[14,621,622],{},"Stream()을 사용하면 전체 생성 시간이 같더라도, 더 빨리 출력되기 때문에 사용자는 빠르다고 느낀다.",[14,624,625],{},"다만 스트리밍에는 반드시 다음 처리가 필요하다.",[69,627,628,631,634,637,640],{},[72,629,630],{},"클라이언트 연결 취소",[72,632,633],{},"전체 타임아웃",[72,635,636],{},"오류 시 대체 메시지",[72,638,639],{},"스트림 종료 처리",[72,641,642],{},"스트리밍 경로의 별도 계측",[10,644,646],{"id":645},"_7-임베딩은-의미를-좌표로-바꾸는-과정","7. 임베딩은 의미를 좌표로 바꾸는 과정",[14,648,649],{},"임베딩은 문장을 숫자 벡터로 변환하는 것",[24,651,654],{"className":652,"code":653,"language":29,"meta":30},[27],"\"강아지\"\n    → [0.12, -0.54, 0.87, ...]\n\n\"반려견\"\n    → [0.14, -0.51, 0.84, ...]\n",[32,655,653],{"__ignoreMap":30},[14,657,658],{},"뜻이 비슷한 문장은 벡터 공간에사도 가깝다.",[14,660,661],{},"VectorStore는 다음을 수행",[24,663,666],{"className":664,"code":665,"language":29,"meta":30},[27],"문서 저장:\n문서 -> 임베딩 -> 벡터 저장\n\n질문 검색\n질문 -> 임베딩 -> 가까운 문서 벡터 검색\n",[32,667,665],{"__ignoreMap":30},[14,669,670],{},"임베딩 모델을 바꾸면 기존 벡터를 그대로 사용할 수 없다.\n-> 차원이 같더라도 의미 공간이 달라질 수 있으므로 전체 문서를 다시 임베딩하고 재색인",[10,672,674],{"id":673},"_8-chunking은-rag-품질을-결정하는-핵심-설계","8. Chunking은 RAG 품질을 결정하는 핵심 설계",[14,676,677],{},"Chunking은 긴 문서를 검색하기 좋은 크기로 나누는 작업",[14,679,680],{},"너무 작으면",[69,682,683,686,689],{},[72,684,685],{},"앞뒤 맥락이 잘린다,",[72,687,688],{},"주어가 무엇인지 알 수 없다.",[72,690,691],{},"조항과 예외 조건이 분리된다.",[14,693,694],{},"너무 크면 :",[69,696,697,700,703],{},[72,698,699],{},"질문과 관련 없는 내용까지 함께 검색된다.",[72,701,702],{},"잡음이 늘어난다.",[72,704,705],{},"모델에 넣는 토큰이 증가한다.",[14,707,708],{},"Overlap은 인접 청그가 일부 문장을 공유하게 하여 경계에서 근거가 잘리는 것을 막는다.",[14,710,711],{},"자료에서는 일반적인 시작값으로 800에서 1200자, 겹침 10에서 20퍼센트를 제시",[91,713,714],{},[14,715,716],{},"질문 하나에 답할 만한 분량인가",[14,718,719],{},"PDF 178페이지의 청킹 표 역시 문서의 구조와 용도에 따라 크기를 바꾸도록 설명",[14,721,722],{},"청크에는 처음부터 다음 메타데이터 넣을 필요 있음",[69,724,725,728,731,734,737,740,743],{},[72,726,727],{},"출처",[72,729,730],{},"페이지",[72,732,733],{},"문서 종류",[72,735,736],{},"부서",[72,738,739],{},"버전",[72,741,742],{},"유효기간",[72,744,745],{},"접근 권한",[14,747,748],{},"메타데이터가 있어야 출처 표시, 권한 필터, 만료 문서 제외, 최신 버전 선택이 가능.",[10,750,752],{"id":751},"_9-rag는-검색과-생성의-결합","9. RAG는 검색과 생성의 결합",[14,754,755],{},"RAG는 두 개의 파이프라인으로 구분",[14,757,758],{},"사전 인덱싱",[24,760,763],{"className":761,"code":762,"language":29,"meta":30},[27],"문서\n  → Reader\n  → Splitter\n  → Chunk\n  → Embedding\n  → VectorStore\n",[32,764,762],{"__ignoreMap":30},[14,766,767],{},"질문별 검색과 생성",[24,769,772],{"className":770,"code":771,"language":29,"meta":30},[27],"질문\n  → 질문 임베딩\n  → 관련 청크 검색\n  → 질문과 근거를 프롬프트에 결합\n  → 모델 응답\n  → 출처 반환\n",[32,773,771],{"__ignoreMap":30},[14,775,776],{},"RAG 파이프라인은 사전 준비 단계인 Indexing과 질문마다 수행되는 Retrieval을 명확히 분리",[14,778,779],{},"RAG 답변이 틀렸을 때는 다음 순서로 확인해야 한다.",[69,781,782,785,788,791,794],{},[72,783,784],{},"관련 문서가 실제로 저장되어 있는가",[72,786,787],{},"검색 결과에 정답 청크가 포함되는가",[72,789,790],{},"정답 청크가 상위에 배치되는가",[72,792,793],{},"메타데이터 필터가 잘못 걸리지 않았는가",[72,795,796],{},"그다음에 프롬프트와 모델을 확인",[91,798,799],{},[14,800,801],{},"검색 결과에 정답이 없다면 프롬프트를 아무리 고쳐도 답은 좋아지지 않는다.",[14,803,804],{},"자료도 품질 문제가 발생하면 모델의 답보다 검색 결과를 먼저 눈으로 확인해야 한다.",[806,807,809],"h3",{"id":808},"rag-고급-전략","RAG 고급 전략",[109,811,812,822],{},[112,813,814],{},[115,815,816,819],{},[118,817,818],{},"문제",[118,820,821],{},"방법",[125,823,824,832,840,848,856,864,872],{},[115,825,826,829],{},[130,827,828],{},"질문과 문서의 표현이 너무 다름",[130,830,831],{},"HyDE",[115,833,834,837],{},[130,835,836],{},"질문이 짧거나 모호함",[130,838,839],{},"질의 변환",[115,841,842,845],{},[130,843,844],{},"정답 문서가 검색되지 않음",[130,846,847],{},"다중 질의 확장",[115,849,850,853],{},[130,851,852],{},"정답 문서가 검색됐지만 순서가 낮음",[130,854,855],{},"재순위",[115,857,858,861],{},[130,859,860],{},"제품 코드처럼 정확한 문자열이 중요함",[130,862,863],{},"Hybrid Search",[115,865,866,869],{},[130,867,868],{},"검색은 작게, 문맥은 크게 넣고 싶음",[130,870,871],{},"Parent-Child",[115,873,874,877],{},[130,875,876],{},"검색 결과가 부족하면 다시 찾고 싶음",[130,878,879],{},"Agentic RAG",[91,881,882],{},[14,883,884],{},"넓게 검색하고, 재순위한 뒤, 좁게 모델에 넣는다.",[14,886,887],{},"예를 들어 20개를 검색하고 재순위한 뒤 상위 4개만 프롬프트에 넣으면 검색 재현율은 유지하면서 잡음과 토큰을 줄일 수 있다.",[14,889,890],{},"RAG 평가는 검색과 생성을 나누어서 측정한다.",[109,892,893,903],{},[112,894,895],{},[115,896,897,900],{},[118,898,899],{},"단계",[118,901,902],{},"지표",[125,904,905,913,920,928],{},[115,906,907,910],{},[130,908,909],{},"검색",[130,911,912],{},"Recall@k",[115,914,915,917],{},[130,916,909],{},[130,918,919],{},"Precision@k",[115,921,922,925],{},[130,923,924],{},"생성",[130,926,927],{},"Faithfulness",[115,929,930,932],{},[130,931,924],{},[130,933,934],{},"Answer Relevancy",[14,936,937],{},"검색 실패와 생성 실패를 한 점수로 뭉치면 어느 부분을 알 수 없기 때문",[58,939,941],{"id":940},"_10-tool-calling은-모델이-실행하는-것이-아님","10. Tool Calling은 모델이 실행하는 것이 아님",[91,943,944],{},[14,945,946],{},"판단은 모델이 하고, 실행은 우리 코드가 함",[14,948,949],{},"모델은 Tool의 이름, 설명, 파라미터 스키마를 보고 어떤 Tool을 호출할지 결정한다. 실제 DB 조회, API 요청, 파일 변경은 Spring 애플리케이션의 메서드가 수행한다.",[14,951,952],{},"모델은 Tool의 이름, 설명, 파라미터 스키마를 보고 어떤 Tool을 호출할지 결정",[14,954,955],{},"실제 DB 조회, API 요청, 파일 변경은 Spring 애플리케이션의 메서드가 수행",[24,957,959],{"className":244,"code":958,"language":246,"meta":30,"style":30},"@Tool(description = \"주문번호로 배송 상태를 조회한다\")\nString orderStatus(\n    @ToolParam(description = \"주문번호\") String orderId,\n    ToolContext context\n) {\n    String userId = context.getContext().get(\"userId\").toString();\n    return orderService.findOwned(orderId, userId);\n}\n",[32,960,961,966,971,976,981,986,991,996],{"__ignoreMap":30},[20,962,963],{"class":251,"line":252},[20,964,965],{},"@Tool(description = \"주문번호로 배송 상태를 조회한다\")\n",[20,967,968],{"class":251,"line":258},[20,969,970],{},"String orderStatus(\n",[20,972,973],{"class":251,"line":264},[20,974,975],{},"    @ToolParam(description = \"주문번호\") String orderId,\n",[20,977,978],{"class":251,"line":270},[20,979,980],{},"    ToolContext context\n",[20,982,983],{"class":251,"line":276},[20,984,985],{},") {\n",[20,987,988],{"class":251,"line":456},[20,989,990],{},"    String userId = context.getContext().get(\"userId\").toString();\n",[20,992,993],{"class":251,"line":499},[20,994,995],{},"    return orderService.findOwned(orderId, userId);\n",[20,997,998],{"class":251,"line":505},[20,999,482],{},[14,1001,1002],{},"Tool 설계에서 중요한 점은 다음과 같다.",[69,1004,1005,1008,1011,1014,1017,1020,1023],{},[72,1006,1007],{},"설명을 명확하게 작성한다.",[72,1009,1010],{},"하나의 Tool은 하나의 책임만 가진다.",[72,1012,1013],{},"입력값을 검증한다.",[72,1015,1016],{},"권한을 Tool 내부에서 검사한다.",[72,1018,1019],{},"실패를 모델이 이해할 수 있는 메시지로 반환한다.",[72,1021,1022],{},"DB Entity 전체가 아니라 필요한 정보만 반환한다.",[72,1024,1025],{},"사용자 ID 같은 보안 정보는 프롬프트가 아니라 ToolContext로 전달",[10,1027,1029],{"id":1028},"_11-gent는-똑똑한-존재가-아니라-반복-구조","11. gent는 똑똑한 존재가 아니라 반복 구조",[14,1031,1032],{},"Agent는 다음 과정을 반복한다.",[24,1034,1037],{"className":1035,"code":1036,"language":29,"meta":30},[27],"Reason\n-> Act\n-> Observe\n-> 다시 Reason\n",[32,1038,1036],{"__ignoreMap":30},[14,1040,1041],{},"Tool calling이 한 번의 도구 사용이면, Agent는 결과를 보고 도구를 다시 선택하는 여러 단계의 Tool Calling",[14,1043,1044],{},"반드시 제한해야 할 것은",[69,1046,1047,1050,1053,1056,1059,1062],{},[72,1048,1049],{},"최대 반복 횟수",[72,1051,1052],{},"최대 토큰",[72,1054,1055],{},"최대 실행 시간",[72,1057,1058],{},"동일 Tool과 동일 인자 반복",[72,1060,1061],{},"Tool별 재시도 횟수",[72,1063,1064],{},"전체 비용 예산",[14,1066,1067],{},"자료는 Agent를 만들 때 잘 수행하는 방법보다, 언제 멈출 지 잘 설정하는 게 더 중요하다.\n(우리의 토큰은 무한하지 않으니까....ㅋㅋㅋ 무한한 토큰이 있다면 시도해보시길)",[14,1069,1070],{},"환불, 삭제, 발송, 취소같이 한 번 실행하면 되돌리기 어려운 작업은 모델이 완료하게 하면 안된다.\n일단은 Tool은 승인 요청만 하고, 실제 처리는 승인 이후에 실행하는 HITL 구조가 좀 필요하다.",[10,1072,1074],{"id":1073},"_12-mcp는-tool-연결의-표준-규격","12. MCP는 Tool 연결의 표준 규격",[14,1076,1077],{},"MCP는 AI 애플리케이션과 외부 Tool 또는 Resource를 연결하는 표준 프로토콜",[24,1079,1082],{"className":1080,"code":1081,"language":29,"meta":30},[27],"MCP Client\n-> 외부 MCP Server의 Tool을 사용\n\nMCP Server\n-> 우리 시스템의 Tool을 다른 AI 애플리케이션에 공개\n",[32,1083,1081],{"__ignoreMap":30},[14,1085,1086],{},"번 MCP 서버로 만들면 여러 AI 애플리케이션이 같은 Tool을 공유할 수 있다.\n다만 원격 실행 통로가 될 수 있으므로 인증과 공개 범위 통제가 필요",[14,1088,1089],{},"MCP는 Spring AI 기초를 배우는 단계에서 가장 먼저 익혀야 할 개념은 아니다. 기본 Tool Calling이 안정된 뒤 Tool 수가 많아지고 다른 애플리케이션과 공유해야 할 때 배우는 것이 적절",[10,1091,1093],{"id":1092},"_13-advisor는-모든-ai-요청이-지나가는-파이프라인","13. Advisor는 모든 AI 요청이 지나가는 파이프라인",[91,1095,1096],{},[14,1097,1098],{},"Advisor는 AOP나 인터셉터와 비슷한 개념",[14,1100,1101],{},"모든 모델 요청에 공통으로 필요한 처리를 한 곳에 모은다.",[69,1103,1104,1107,1110,1113,1116,1119,1122],{},[72,1105,1106],{},"안전 필터",[72,1108,1109],{},"대화 메모리",[72,1111,1112],{},"RAG 근거 주입",[72,1114,1115],{},"감사 로그",[72,1117,1118],{},"토큰 계측",[72,1120,1121],{},"지연 시간 측정",[72,1123,1124],{},"응답 후처리",[14,1126,1127],{},"권장 흐름",[14,1129,1130],{},"요청:",[24,1132,1135],{"className":1133,"code":1134,"language":29,"meta":30},[27],"Audit\n  → Safety\n  → Memory\n  → RAG\n  → Logger\n  → Model\n\n응답:\nModel\n  → Logger\n  → RAG\n  → Memory\n  → Safety\n  → Audit\n",[32,1136,1134],{"__ignoreMap":30},[14,1138,1139],{},"Safety가 Memory 뒤에 있다면, 차단해야 할 공격 문장이 이미 대화 이략에 저장될 수 있음\n따라서 안전 필터는 메모리 저장보다 앞에 있어야 한다.",[10,1141,1143],{"id":1142},"_14-memory는-저장소와-보존-정책까지-포함하는-개념","14. Memory는 저장소와 보존 정책까지 포함하는 개념",[14,1145,1146],{},"ChatMemory는 이전 메시지를 저장하고 새 요청에 다시 주입",[14,1148,1149],{},"개발 단계에서는 InMemory를 사용할 수 있지만, 운영에서는 다음을 고려해야 한다.",[109,1151,1152,1162],{},[112,1153,1154],{},[115,1155,1156,1159],{},[118,1157,1158],{},"저장소",[118,1160,1161],{},"적합한 상황",[125,1163,1164,1172,1180,1188],{},[115,1165,1166,1169],{},[130,1167,1168],{},"InMemory",[130,1170,1171],{},"로컬 개발, 테스트",[115,1173,1174,1177],{},[130,1175,1176],{},"JDBC",[130,1178,1179],{},"일반적인 영속 운영",[115,1181,1182,1185],{},[130,1183,1184],{},"Redis",[130,1186,1187],{},"다중 인스턴스, 짧은 TTL",[115,1189,1190,1193],{},[130,1191,1192],{},"Cassandra",[130,1194,1195],{},"초대량 장기 저장",[14,1197,1198],{},"대화가 길어지면 모든 메시지를 계속 넣을 수 없으므로 다음 전략이 필요",[14,1200,1201],{},"최근 N개 메시지만 유지\n오래된 대화를 요약\n중요 결정만 별도 저장\n대화 보존 기간 설정\n사용자별 삭제 기능\n개인정보 마스킹",[14,1203,1204],{},"대화 이력에는 개인정보가 빠르게 쌓이므로 저장소를 선택할 때 보존 기간과 삭제 절차도 동시에 설계해야 한다.\n(정말 고려해야 할 것들이 많다!)",[10,1206,1208],{"id":1207},"_15-운영에서는-모델-품질만-보면-안-되고-다른-것들도-같이-고려해야-한다","15. 운영에서는 모델 품질만 보면 안 되고, 다른 것들도 같이 고려해야 한다.",[14,1210,1211],{},"운영 지표는 최소한 다음은 측정해야 한다.",[69,1213,1214,1217,1220,1223,1226,1229,1232,1235,1238,1241],{},[72,1215,1216],{},"입력 토큰",[72,1218,1219],{},"출력 토큰",[72,1221,1222],{},"총 토큰",[72,1224,1225],{},"첫 토큰까지의 시간",[72,1227,1228],{},"전체 응답 시간",[72,1230,1231],{},"RAG 검색 시간",[72,1233,1234],{},"Tool 호출 횟수",[72,1236,1237],{},"Tool 실패율",[72,1239,1240],{},"모델 오류율",[72,1242,1243],{},"풀백 발생 횟수",[14,1245,1246],{},"AI 호출은 외부 의존성이므로 주 공급자가 실패하더라도 서비스 전체가 죽지 않도록 다음 폴백을 준비해야 한다.",[24,1248,1251],{"className":1249,"code":1250,"language":29,"meta":30},[27],"주 모델 실패\n-> 보조 모델\n-> 캐시\n-> 정형 응답\n-> 최소 기능 응답\n",[32,1252,1250],{"__ignoreMap":30},[10,1254,1255],{"id":1255},"요약",[58,1257,1259],{"id":1258},"반드시-머릿속에-있어야-하는-개념","반드시 머릿속에 있어야 하는 개념",[109,1261,1262,1272],{},[112,1263,1264],{},[115,1265,1266,1269],{},[118,1267,1268],{},"익힐 것",[118,1270,1271],{},"이해 완료 기준",[125,1273,1274,1282,1290,1298,1306,1314,1322,1329],{},[115,1275,1276,1279],{},[130,1277,1278],{},"Controller, Service, Repository 역할",[130,1280,1281],{},"ChatClient가 왜 Service에 있어야 하는지 설명할 수 있다",[115,1283,1284,1287],{},[130,1285,1286],{},"LLM의 확률성과 무상태성",[130,1288,1289],{},"Memory가 왜 이력을 다시 넣는 기능인지 설명할 수 있다",[115,1291,1292,1295],{},[130,1293,1294],{},"ChatModel과 ChatClient 차이",[130,1296,1297],{},"실무에서 ChatClient를 쓰는 이유를 말할 수 있다",[115,1299,1300,1303],{},[130,1301,1302],{},"Embedding과 VectorStore",[130,1304,1305],{},"키워드가 달라도 검색되는 이유를 설명할 수 있다",[115,1307,1308,1311],{},[130,1309,1310],{},"RAG의 두 단계",[130,1312,1313],{},"Indexing과 Retrieval을 구분할 수 있다",[115,1315,1316,1319],{},[130,1317,1318],{},"Tool Calling",[130,1320,1321],{},"판단은 모델, 실행은 코드라고 설명할 수 있다",[115,1323,1324,1326],{},[130,1325,172],{},[130,1327,1328],{},"순서가 왜 보안 정책이 되는지 설명할 수 있다",[115,1330,1331,1334],{},[130,1332,1333],{},"Agent",[130,1335,1336],{},"반복 횟수와 비용 상한이 필요한 이유를 설명할 수 있다",[58,1338,1340],{"id":1339},"반드시-한-번은-직접-구현해야-하는-것","반드시 한 번은 직접 구현해야 하는 것",[14,1342,1343],{},"(이거는 주말에 한 번 해보면 될 듯하다.)",[109,1345,1346,1356],{},[112,1347,1348],{},[115,1349,1350,1353],{},[118,1351,1352],{},"구현할 것",[118,1354,1355],{},"완료 기준",[125,1357,1358,1366,1376,1387,1395,1403,1411,1429,1437,1445],{},[115,1359,1360,1363],{},[130,1361,1362],{},"용도별 ChatClient 빈",[130,1364,1365],{},"요약용과 창작용의 옵션을 다르게 설정",[115,1367,1368,1370],{},[130,1369,299],{},[130,1371,1372,1375],{},[32,1373,1374],{},"entity()","로 record와 enum 반환",[115,1377,1378,1381],{},[130,1379,1380],{},"SSE 스트리밍",[130,1382,1383,1386],{},[32,1384,1385],{},"stream()","에 timeout과 cancel 처리",[115,1388,1389,1392],{},[130,1390,1391],{},"RAG 인제스트",[130,1393,1394],{},"문서 읽기, 청킹, 임베딩, VectorStore 적재",[115,1396,1397,1400],{},[130,1398,1399],{},"RAG 검색",[130,1401,1402],{},"topK, threshold, metadata filter 적용",[115,1404,1405,1408],{},[130,1406,1407],{},"출처 반환",[130,1409,1410],{},"답변과 함께 문서명 또는 페이지 반환",[115,1412,1413,1416],{},[130,1414,1415],{},"Tool",[130,1417,1418,1421,1422,1421,1425,1428],{},[32,1419,1420],{},"@Tool",", ",[32,1423,1424],{},"@ToolParam",[32,1426,1427],{},"ToolContext"," 적용",[115,1430,1431,1434],{},[130,1432,1433],{},"승인 게이트",[130,1435,1436],{},"환불이나 삭제를 실행하지 않고 승인 요청으로 접수",[115,1438,1439,1442],{},[130,1440,1441],{},"Advisor 체인",[130,1443,1444],{},"Safety가 Memory보다 앞에 오도록 구성",[115,1446,1447,1450],{},[130,1448,1449],{},"운영 지표",[130,1451,1452],{},"토큰, 지연, Tool 호출을 Micrometer로 측정",[58,1454,1456],{"id":1455},"기본-rag와-tool이-완성된-뒤-배우기","기본 RAG와 Tool이 완성된 뒤 배우기",[14,1458,1459],{},"(다음 포스팅에서 제공할 예정)",[14,1461,1462],{},"HyDE\nAgentic RAG\nParent-Child Retrieval\nContextual Retrieval\nMulti-Agent\nMCP Server\n공급자별 고유 옵션\n복잡한 Evaluator-Optimizer 루프",[10,1464,1466],{"id":1465},"효율적인-학습-순서","효율적인 학습 순서",[58,1468,1470],{"id":1469},"_1-기본-ai-api-만들기","1. 기본 AI API 만들기",[24,1472,1475],{"className":1473,"code":1474,"language":29,"meta":30},[27],"Controller\n  → Service\n  → ChatClient\n  → 문자열 또는 DTO 반환\n",[32,1476,1474],{"__ignoreMap":30},[69,1478,1479,1482,1485,1488],{},[72,1480,1481],{},"Controller에 ChatClient가 없다.",[72,1483,1484],{},"ChatClient는 Config에서 빈으로 생성된다.",[72,1486,1487],{},"API Key는 환경변수로 주입된다.",[72,1489,1490],{},"예외가 안전한 응답으로 변환된다.",[58,1492,1494],{"id":1493},"_2-출력-품질과-형식-제어","2. 출력 품질과 형식 제어",[69,1496,1497,1500,1503,1506,1509],{},[72,1498,1499],{},"역할, 맥락, 지시, 형식이 있는 프롬프트 작성",[72,1501,1502],{},"Few-shot 추가",[72,1504,1505],{},"temperature 실험",[72,1507,1508],{},"entity() 구조화 출력",[72,1510,1511],{},"stream() SSE 출력",[14,1513,1514],{},"완료 기준:",[69,1516,1517,1520,1523],{},[72,1518,1519],{},"문자열 파싱 코드가 없다.",[72,1521,1522],{},"같은 분류 입력에 결과가 안정적이다.",[72,1524,1525],{},"스트리밍 취소와 타임아웃이 동작한다.",[58,1527,1529],{"id":1528},"_3-rag-완성","3. RAG 완성",[24,1531,1534],{"className":1532,"code":1533,"language":29,"meta":30},[27],"문서\n  → Chunk\n  → Embedding\n  → VectorStore\n  → 검색\n  → 출처가 있는 답변\n",[32,1535,1533],{"__ignoreMap":30},[69,1537,1538,1541,1544,1547],{},[72,1539,1540],{},"검색 결과를 별도 API로 확인할 수 있다.",[72,1542,1543],{},"검색된 청크의 출처와 점수가 보인다.",[72,1545,1546],{},"문서에 없는 질문에는 모른다고 답한다.",[72,1548,1549],{},"문서 재색인 시 중복 청크가 쌓이지 않는다.",[58,1551,1553],{"id":1552},"_4-tool과-운영-완성","4. Tool과 운영 완성",[14,1555,1556],{},"목표:\n실시간 주문 조회 Tool\n사용자 권한 검증\n환불 승인 게이트\nMemory와 Advisor\n토큰과 지연 계측\n폴백 응답\n완료 기준:\n다른 사용자의 데이터를 조회할 수 없다.\n위험 작업은 즉시 실행되지 않는다.\nSafety가 Memory보다 먼저 실행된다.\n모델 장애가 발생해도 기본 서비스는 유지된다",[1558,1559,1560],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":30,"searchDepth":258,"depth":258,"links":1562},[1563,1564,1567,1568,1569,1570,1571,1572,1573,1574],{"id":60,"depth":258,"text":61},{"id":103,"depth":258,"text":104,"children":1565},[1566],{"id":808,"depth":264,"text":809},{"id":940,"depth":258,"text":941},{"id":1258,"depth":258,"text":1259},{"id":1339,"depth":258,"text":1340},{"id":1455,"depth":258,"text":1456},{"id":1469,"depth":258,"text":1470},{"id":1493,"depth":258,"text":1494},{"id":1528,"depth":258,"text":1529},{"id":1552,"depth":258,"text":1553},[1576,1577],"Backend","SPRING_AI","2026-08-19 15:35:50 +0900","md",null,"\u002Fbackend\u002Fspring_ai\u002F2026\u002F08\u002F19\u002F요약\u002F",{"layout":1583},"post","\u002Fposts\u002Fbackend\u002Fspring_ai",{"title":5,"description":16},"posts\u002FBackend\u002FSPRING_AI \u002F요약",[],"lDiy_LD4of0wdNze5TLTxXcsPL1AkL3joBF9vHSo6j0",1788744785829]