8. ์›น์‚ฌ์ดํŠธ ์ „์ฒด๋ฅผ ์•„๋Š” ์ฑ—๋ด‡, SiteGPT

๐ŸŽฏ ์ด ์ฑ•ํ„ฐ์—์„œ ๋ฐฐ์šธ ๊ฒƒ

  • SitemapLoader๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์›น์‚ฌ์ดํŠธ์˜ ์‚ฌ์ดํŠธ๋งต(sitemap.xml)์„ ์ฝ๊ณ  ๋ชจ๋“  ํŽ˜์ด์ง€์˜ URL์„ ์ž๋™์œผ๋กœ ์ˆ˜์ง‘ํ•˜๋Š” ๋ฐฉ๋ฒ•
  • BeautifulSoup๊ณผ ํŒŒ์‹ฑ ํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์Šคํฌ๋ž˜ํ•‘ํ•œ HTML์—์„œ ๋ถˆํ•„์š”ํ•œ ๋ถ€๋ถ„(ํ—ค๋”, ํ‘ธํ„ฐ ๋“ฑ)์„ ์ œ๊ฑฐํ•˜๊ณ  ์ˆœ์ˆ˜ ํ…์ŠคํŠธ๋งŒ ์ถ”์ถœํ•˜๋Š” ๋ฐฉ๋ฒ•
  • Map-ReRank ์ฒด์ธ: ๊ฒ€์ƒ‰๋œ ์—ฌ๋Ÿฌ ๋ฌธ์„œ ๊ฐ๊ฐ์— ๋Œ€ํ•ด ๋‹ต๋ณ€๊ณผ ์ ์ˆ˜๋ฅผ ์ƒ์„ฑ(Map)ํ•œ ํ›„, ๊ฐ€์žฅ ์ ์ˆ˜๊ฐ€ ๋†’์€ ๋‹ต๋ณ€์„ ์„ ํƒํ•˜์—ฌ ์ตœ์ข… ๋‹ต๋ณ€์„ ๊ตฌ์„ฑ(ReRank)ํ•˜๋Š” ๊ณ ๊ธ‰ RAG ๊ธฐ๋ฒ•
  • LCEL์„ ์‚ฌ์šฉํ•˜์—ฌ Map-ReRank ์ฒด์ธ์„ ์ง์ ‘ ๊ตฌํ˜„ํ•˜๋Š” ๋ฐฉ๋ฒ•
  • Streamlit์„ ์‚ฌ์šฉํ•˜์—ฌ SiteGPT์˜ ์ „์ฒด UI์™€ ์ƒํ˜ธ์ž‘์šฉ ๋กœ์ง์„ ์™„์„ฑํ•˜๋Š” ๋ฐฉ๋ฒ•

ํšจ์œจ์ ์ธ ์›น ์Šคํฌ๋ž˜ํ•‘

๐ŸŽฏ ์ด๋ฒˆ ๋‹จ๊ณ„์—์„œ ๋ฐฐ์šธ ๊ฒƒ

  • AsyncChromiumLoader์˜ ํ•œ๊ณ„์™€ SitemapLoader์˜ ํ•„์š”์„ฑ์„ ์ดํ•ดํ•ฉ๋‹ˆ๋‹ค.
  • SitemapLoader๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์›น์‚ฌ์ดํŠธ์˜ ๋ชจ๋“  ํŽ˜์ด์ง€๋ฅผ ํšจ์œจ์ ์œผ๋กœ ๋กœ๋“œํ•ฉ๋‹ˆ๋‹ค.
  • ์‚ฌ์šฉ์ž ์ •์˜ ํŒŒ์‹ฑ ํ•จ์ˆ˜์™€ BeautifulSoup์„ ์‚ฌ์šฉํ•˜์—ฌ HTML์„ ์ •๋ฆฌํ•˜๊ณ  ์›ํ•˜๋Š” ํ…์ŠคํŠธ๋งŒ ์ถ”์ถœํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ“ 1๋‹จ๊ณ„: Sitemap์œผ๋กœ ์›น์‚ฌ์ดํŠธ ์ „์ฒด ๋กœ๋“œํ•˜๊ธฐ

์ „์ฒด ์ฝ”๋“œ (pages/04_SiteGPT.py):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
from langchain.document_loaders import SitemapLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
import streamlit as st

# 1. HTML์—์„œ ๋ถˆํ•„์š”ํ•œ ๋ถ€๋ถ„์„ ์ œ๊ฑฐํ•˜๋Š” ํŒŒ์‹ฑ ํ•จ์ˆ˜
def parse_page(soup):
header = soup.find("header")
footer = soup.find("footer")
if header:
header.decompose() # <header> ํƒœ๊ทธ ์ œ๊ฑฐ
if footer:
footer.decompose() # <footer> ํƒœ๊ทธ ์ œ๊ฑฐ
return (
str(soup.get_text())
.replace("\n", " ")
.replace("\xa0", " ")
.replace("CloseSearch Submit Blog", "")
)

@st.cache_data(show_spinner="Loading website...")
def load_website(url):
splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(
chunk_size=1000,
chunk_overlap=200,
)
# 2. SitemapLoader ์ดˆ๊ธฐํ™”
loader = SitemapLoader(
url, # sitemap.xml ํŒŒ์ผ์˜ URL
filter_urls=[r"^(.*\/blog\/).*"], # ํŠน์ • ํŒจํ„ด์˜ URL๋งŒ ํ•„ํ„ฐ๋ง (์„ ํƒ ์‚ฌํ•ญ)
parsing_function=parse_page, # ๊ฐ ํŽ˜์ด์ง€์— ์ ์šฉํ•  ํŒŒ์‹ฑ ํ•จ์ˆ˜
)
loader.requests_per_second = 2 # ์„œ๋ฒ„ ๋ถ€ํ•˜๋ฅผ ์ค„์ด๊ธฐ ์œ„ํ•ด ์š”์ฒญ ์†๋„ ์กฐ์ ˆ
docs = loader.load_and_split(text_splitter=splitter)
# ... ๋ฒกํ„ฐ ์Šคํ† ์–ด ์ƒ์„ฑ ๋กœ์ง ...

๐Ÿ” ์ฝ”๋“œ ์ƒ์„ธ ์„ค๋ช…

1. SitemapLoader

  • AsyncChromiumLoader๊ฐ€ ๋‹จ์ผ ํŽ˜์ด์ง€๋งŒ ๋กœ๋“œํ•˜๋Š” ๋ฐ˜๋ฉด, SitemapLoader๋Š” ์›น์‚ฌ์ดํŠธ์˜ sitemap.xml ํŒŒ์ผ์„ ๋ถ„์„ํ•˜์—ฌ ์‚ฌ์ดํŠธ ๋‚ด์˜ ๋ชจ๋“  ํŽ˜์ด์ง€ URL ๋ชฉ๋ก์„ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค. ๊ทธ ํ›„ ๊ฐ URL์„ ๋ฐฉ๋ฌธํ•˜์—ฌ ์ฝ˜ํ…์ธ ๋ฅผ ์Šคํฌ๋ž˜ํ•‘ํ•ฉ๋‹ˆ๋‹ค.
  • ์™œ ์‚ฌ์šฉํ•˜๋Š”๊ฐ€?: ๋‹จ ํ•˜๋‚˜์˜ URL(sitemap.xml)๋งŒ์œผ๋กœ ์›น์‚ฌ์ดํŠธ ์ „์ฒด๋ฅผ ํ•™์Šต ๋ฐ์ดํ„ฐ๋กœ ๋งŒ๋“ค ์ˆ˜ ์žˆ์–ด ๋งค์šฐ ํšจ์œจ์ ์ž…๋‹ˆ๋‹ค.
  • filter_urls: ์ •๊ทœ์‹์„ ์‚ฌ์šฉํ•˜์—ฌ ์›ํ•˜๋Š” URL ํŒจํ„ด(์˜ˆ: ๋ธ”๋กœ๊ทธ ํฌ์ŠคํŠธ๋งŒ)์„ ๊ฐ€์ง„ ํŽ˜์ด์ง€๋งŒ ์Šคํฌ๋ž˜ํ•‘ํ•˜๋„๋ก ํ•„ํ„ฐ๋งํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

2. ์‚ฌ์šฉ์ž ์ •์˜ ํŒŒ์‹ฑ ํ•จ์ˆ˜ (parsing_function)

  • SitemapLoader๋Š” ๊ฐ ํŽ˜์ด์ง€์˜ HTML ์ฝ˜ํ…์ธ ๋ฅผ BeautifulSoup ๊ฐ์ฒด(soup)๋กœ ๋ณ€ํ™˜ํ•˜์—ฌ parsing_function์— ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค.
  • ์ด ํ•จ์ˆ˜ ๋‚ด์—์„œ soup.find(...).decompose()์™€ ๊ฐ™์€ ๋ฉ”์†Œ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๊ด‘๊ณ , ๋„ค๋น„๊ฒŒ์ด์…˜ ๋ฐ”, ํ‘ธํ„ฐ ๋“ฑ ๋‹ต๋ณ€ ์ƒ์„ฑ์— ๋ฐฉํ•ด๊ฐ€ ๋˜๋Š” ๋ถˆํ•„์š”ํ•œ HTML ์š”์†Œ๋ฅผ ์ œ๊ฑฐํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋ฅผ ํ†ตํ•ด ๋” ๊นจ๋—ํ•˜๊ณ  ๊ด€๋ จ์„ฑ ๋†’์€ ํ…์ŠคํŠธ๋งŒ ์ถ”์ถœํ•˜์—ฌ LLM์— ์ œ๊ณตํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

โœ… ์ฒดํฌ๋ฆฌ์ŠคํŠธ

  • beautifulsoup4 ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์„ค์น˜ํ–ˆ๋‚˜์š”?
  • ๋Œ€์ƒ ์›น์‚ฌ์ดํŠธ์˜ sitemap.xml URL์„ ์ฐพ์•˜๋‚˜์š”? (๋ณดํ†ต https://example.com/sitemap.xml)
  • SitemapLoader๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์‚ฌ์ดํŠธ๋งต์„ ๋กœ๋“œํ•˜๊ณ , parsing_function์œผ๋กœ HTML์„ ์ •๋ฆฌํ–ˆ๋‚˜์š”?

Map-ReRank ์ฒด์ธ ๊ตฌํ˜„ํ•˜๊ธฐ

๐ŸŽฏ ์ด๋ฒˆ ๋‹จ๊ณ„์—์„œ ๋ฐฐ์šธ ๊ฒƒ

  • Map-ReRank ์ฒด์ธ์˜ ๋™์ž‘ ์›๋ฆฌ๋ฅผ ์ดํ•ดํ•ฉ๋‹ˆ๋‹ค.
  • Map ๋‹จ๊ณ„: ๊ฒ€์ƒ‰๋œ ๊ฐ ๋ฌธ์„œ์— ๋Œ€ํ•ด ๊ฐœ๋ณ„์ ์œผ๋กœ ๋‹ต๋ณ€๊ณผ ์ ์ˆ˜๋ฅผ ์ƒ์„ฑํ•˜๋Š” ์ฒด์ธ์„ ๋งŒ๋“ญ๋‹ˆ๋‹ค.
  • ReRank (Reduce) ๋‹จ๊ณ„: ์ƒ์„ฑ๋œ ์—ฌ๋Ÿฌ ๋‹ต๋ณ€๊ณผ ์ ์ˆ˜๋ฅผ ๋ณด๊ณ , ๊ฐ€์žฅ ์‹ ๋ขฐ๋„ ๋†’์€ ์ตœ์ข… ๋‹ต๋ณ€์„ ์„ ํƒ(๋˜๋Š” ์ข…ํ•ฉ)ํ•˜๋Š” ์ฒด์ธ์„ ๋งŒ๋“ญ๋‹ˆ๋‹ค.

๐Ÿ“ 1๋‹จ๊ณ„: Map-ReRank ๋กœ์ง ๊ตฌํ˜„

์ „์ฒด ์ฝ”๋“œ (pages/04_SiteGPT.py):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# ... (imports, llm ์„ค์ •)

# 1. Map ๋‹จ๊ณ„ ํ”„๋กฌํ”„ํŠธ: ๊ฐ ๋ฌธ์„œ์— ๋Œ€ํ•ด ๋‹ต๋ณ€๊ณผ ์ ์ˆ˜๋ฅผ ์ƒ์„ฑ
answers_prompt = ChatPromptTemplate.from_template(
"""Using ONLY the following context answer the user's question. ... Then, give a score to the answer between 0 and 5. ... Context: {context} ... Question: {question}"""
)

# Map ๋‹จ๊ณ„ ์‹คํ–‰ ํ•จ์ˆ˜
def get_answers(inputs):
docs = inputs["docs"]
question = inputs["question"]
answers_chain = answers_prompt | llm
return {
"question": question,
"answers": [
{
"answer": answers_chain.invoke({"question": question, "context": doc.page_content}).content,
"source": doc.metadata["source"],
"date": doc.metadata["lastmod"],
}
for doc in docs
],
}

# 2. ReRank(Reduce) ๋‹จ๊ณ„ ํ”„๋กฌํ”„ํŠธ: ์—ฌ๋Ÿฌ ๋‹ต๋ณ€ ์ค‘ ์ตœ๊ณ ๋ฅผ ์„ ํƒ
choose_prompt = ChatPromptTemplate.from_messages([
("system", "Use ONLY the following pre-existing answers to answer the user's question. Use the answers that have the highest score ... Cite sources ... Answers: {answers}"),
("human", "{question}"),
])

# ReRank ๋‹จ๊ณ„ ์‹คํ–‰ ํ•จ์ˆ˜
def choose_answer(inputs):
answers = inputs["answers"]
question = inputs["question"]
choose_chain = choose_prompt | llm
condensed = "\n\n".join(f"{answer['answer']}\nSource:{answer['source']}\nDate:{answer['date']}\n" for answer in answers)
return choose_chain.invoke({"question": question, "answers": condensed})

# 3. ์ตœ์ข… ์ฒด์ธ ๊ฒฐํ•ฉ
chain = (
{"docs": retriever, "question": RunnablePassthrough()}
| RunnableLambda(get_answers)
| RunnableLambda(choose_answer)
)

๐Ÿ” ์ฝ”๋“œ ์ƒ์„ธ ์„ค๋ช…

1. Map-ReRank์˜ ๋™์ž‘ ์›๋ฆฌ
stuff ๋ฐฉ์‹์ด ๋ชจ๋“  ๋ฌธ์„œ๋ฅผ ํ•œ ๋ฒˆ์— ์ปจํ…์ŠคํŠธ์— ๋„ฃ๋Š” ๊ฒƒ๊ณผ ๋‹ฌ๋ฆฌ, Map-ReRank๋Š” ๋” ์ •๊ตํ•œ ์ ‘๊ทผ ๋ฐฉ์‹์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

  • Map: ๊ฒ€์ƒ‰๋œ ๋ฌธ์„œ๊ฐ€ 5๊ฐœ๋ผ๋ฉด, 5๊ฐœ์˜ ๋ฌธ์„œ ๊ฐ๊ฐ์— ๋Œ€ํ•ด answers_prompt๋ฅผ ์‹คํ–‰ํ•˜์—ฌ 5๊ฐœ์˜ ๋…๋ฆฝ์ ์ธ ๋‹ต๋ณ€๊ณผ ์ ์ˆ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. ์ด๋Š” ๊ฐ ๋ฌธ์„œ๊ฐ€ ์งˆ๋ฌธ์— ์–ผ๋งˆ๋‚˜ ๊ด€๋ จ์ด ์žˆ๋Š”์ง€๋ฅผ ํ‰๊ฐ€ํ•˜๋Š” ๊ณผ์ •์ž…๋‹ˆ๋‹ค.

  • ReRank (Reduce): ์ƒ์„ฑ๋œ 5๊ฐœ์˜ ๋‹ต๋ณ€+์ ์ˆ˜ ํ…์ŠคํŠธ๋ฅผ ๋ชจ๋‘ ๋ชจ์•„ choose_prompt์˜ ์ปจํ…์ŠคํŠธ({answers})๋กœ ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  LLM์—๊ฒŒ ์ด ์ค‘์—์„œ ๊ฐ€์žฅ ์ ์ˆ˜๊ฐ€ ๋†’๊ณ  ์ตœ์‹  ์ •๋ณด์ธ ๊ฒƒ์„ ๋ฐ”ํƒ•์œผ๋กœ ์ตœ์ข… ๋‹ต๋ณ€์„ ์„ ํƒํ•˜๊ฑฐ๋‚˜ ์ข…ํ•ฉํ•˜๋ผ๊ณ  ์ง€์‹œํ•ฉ๋‹ˆ๋‹ค.

  • ์™œ ์‚ฌ์šฉํ•˜๋Š”๊ฐ€?: stuff ๋ฐฉ์‹๋ณด๋‹ค ๋” ๋งŽ์€ ๋ฌธ์„œ๋ฅผ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ๊ฐ ๋ฌธ์„œ์˜ ๊ด€๋ จ์„ฑ์„ ๊ฐœ๋ณ„์ ์œผ๋กœ ํ‰๊ฐ€ํ•˜๋ฏ€๋กœ ์ตœ์ข… ๋‹ต๋ณ€์˜ ์ •ํ™•์„ฑ๊ณผ ์‹ ๋ขฐ๋„๊ฐ€ ๋†’์•„์ง‘๋‹ˆ๋‹ค. LLM์ด ์—ฌ๋Ÿฌ ์†Œ์Šค๋ฅผ ๋น„๊ตํ•˜๊ณ  ๋Œ€์กฐํ•˜์—ฌ ๊ฐ€์žฅ ์ข‹์€ ๋‹ต๋ณ€์„ ์ฐพ๋„๋ก ์œ ๋„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

โœ… ์ฒดํฌ๋ฆฌ์ŠคํŠธ

  • Map ๋‹จ๊ณ„์— ํ•ด๋‹นํ•˜๋Š” answers_prompt์™€ get_answers ํ•จ์ˆ˜๋ฅผ ์ž‘์„ฑํ–ˆ๋‚˜์š”?
  • ReRank ๋‹จ๊ณ„์— ํ•ด๋‹นํ•˜๋Š” choose_prompt์™€ choose_answer ํ•จ์ˆ˜๋ฅผ ์ž‘์„ฑํ–ˆ๋‚˜์š”?
  • LCEL์„ ์‚ฌ์šฉํ•˜์—ฌ retriever -> get_answers -> choose_answer ์ˆœ์„œ๋กœ ์ „์ฒด ์ฒด์ธ์„ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์—ฐ๊ฒฐํ–ˆ๋‚˜์š”?

SiteGPT UI ์™„์„ฑ

๐ŸŽฏ ์ด๋ฒˆ ๋‹จ๊ณ„์—์„œ ๋ฐฐ์šธ ๊ฒƒ

  • st.text_input์„ ์‚ฌ์šฉํ•˜์—ฌ ์‚ฌ์šฉ์ž๋กœ๋ถ€ํ„ฐ ์งˆ๋ฌธ์„ ๋ฐ›์Šต๋‹ˆ๋‹ค.
  • ์™„์„ฑ๋œ Map-ReRank ์ฒด์ธ์„ ์‹คํ–‰ํ•˜๊ณ , ์ตœ์ข… ๊ฒฐ๊ณผ๋ฅผ st.markdown์œผ๋กœ ํ™”๋ฉด์— ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ“ 1๋‹จ๊ณ„: Streamlit UI์™€ ์ฒด์ธ ์—ฐ๋™

์ „์ฒด ์ฝ”๋“œ (pages/04_SiteGPT.py):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# ... (์ด์ „ ์ฝ”๋“œ)

if url:
if ".xml" not in url:
# ... ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ ...
else:
retriever = load_website(url)
query = st.text_input("Ask a question to the website.")
if query:
chain = (
{
"docs": retriever,
"question": RunnablePassthrough(),
}
| RunnableLambda(get_answers)
| RunnableLambda(choose_answer)
)
result = chain.invoke(query)
st.markdown(result.content.replace("$", "\$")) # $ ๋ฌธ์ž๊ฐ€ LaTeX๋กœ ๋ Œ๋”๋ง๋˜๋Š” ๊ฒƒ์„ ๋ฐฉ์ง€

๐Ÿ” ์ฝ”๋“œ ์ƒ์„ธ ์„ค๋ช…

์ด์ œ ๋ชจ๋“  ์กฐ๊ฐ์ด ๋งž์ถฐ์กŒ์Šต๋‹ˆ๋‹ค.

  1. ์‚ฌ์šฉ์ž๊ฐ€ ์‚ฌ์ด๋“œ๋ฐ”์— sitemap.xml URL์„ ์ž…๋ ฅํ•ฉ๋‹ˆ๋‹ค.
  2. load_website(url) ํ•จ์ˆ˜๊ฐ€ ํ˜ธ์ถœ๋˜์–ด ์›น์‚ฌ์ดํŠธ ์ „์ฒด๋ฅผ ๋กœ๋“œํ•˜๊ณ , ๋ถ„ํ• ํ•˜๊ณ , ์ž„๋ฒ ๋”ฉํ•˜์—ฌ retriever๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. ์ด ๊ณผ์ •์€ @st.cache_data ๋•๋ถ„์— URL๋‹น ํ•œ ๋ฒˆ๋งŒ ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค.
  3. ์‚ฌ์šฉ์ž๊ฐ€ st.text_input์— ์งˆ๋ฌธ(query)์„ ์ž…๋ ฅํ•ฉ๋‹ˆ๋‹ค.
  4. ์งˆ๋ฌธ์ด ์ž…๋ ฅ๋˜๋ฉด, ์šฐ๋ฆฌ๊ฐ€ ๊ตฌ์„ฑํ•œ Map-ReRank ์ฒด์ธ์ด invoke(query)๋กœ ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค.
  5. ์ฒด์ธ์˜ ์ตœ์ข… ๊ฒฐ๊ณผ(result.content)๊ฐ€ st.markdown์„ ํ†ตํ•ด ์‚ฌ์šฉ์ž์—๊ฒŒ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค.

โœ… ์ฒดํฌ๋ฆฌ์ŠคํŠธ

  • st.text_input์„ ์‚ฌ์šฉํ•˜์—ฌ ์‚ฌ์šฉ์ž ์งˆ๋ฌธ์„ ๋ฐ›๋Š” UI๋ฅผ ์ถ”๊ฐ€ํ–ˆ๋‚˜์š”?
  • ์งˆ๋ฌธ์ด ์ž…๋ ฅ๋˜์—ˆ์„ ๋•Œ, ์™„์„ฑ๋œ ์ฒด์ธ์„ invokeํ•˜๊ณ  ๊ทธ ๊ฒฐ๊ณผ๋ฅผ ํ™”๋ฉด์— ์ถœ๋ ฅํ–ˆ๋‚˜์š”?
  • SiteGPT๊ฐ€ ์›น์‚ฌ์ดํŠธ ์ฝ˜ํ…์ธ ์— ๊ธฐ๋ฐ˜ํ•˜์—ฌ ์ •ํ™•ํ•œ ๋‹ต๋ณ€๊ณผ ์ถœ์ฒ˜๋ฅผ ์ œ๊ณตํ•˜๋Š”์ง€ ํ…Œ์ŠคํŠธํ–ˆ๋‚˜์š”?

์ถœ์ฒ˜ : https://nomadcoders.co/fullstack-gpt