Самавыпраўляльны агент дакументацыі на LangGraph і Ollama
Лакальны рэфлексіўны канвеер: адна мадэль піша чарнавік дакументацыі Python API, другая яго крытыкуе, трэцяя ўзгадняе вынік — усё як цыкл LangGraph на Ollama.
Крыніцы: Build a Self-Correcting AI Agent with LangGraph and Ollama
Гэты падчынны метод называецца рэфлексіяй. Яго сутніса заключаецца у тым, што большыя мовныя модулі значна лепшая выкрываюць аберанціі ў тэксце, чым можуць зразу напісаць бездоганны тэкст, таму праца дзеліцца на разныя ролі. Існуе важлівы тэчка: кожная роль выкорыстоўвае адны разны местны мадэль. Мадэль, якая як пішае, так і оцэнюе, часта проста праглядае свой сабе ж вынік, тады як окольны крытык узбегае ад гэтаго.
Ранейшыя агенты, створаныя з нуля без спецыяльных фрэймворкаў, стверджвалі, што такія інструменты, як LangGraph, становяцца корыстнымі толькі пасля таго, як вы самі створылі простую версію. Часоў гэты раз LangGraph ёсць свядомым выборам, адколі цыкл перагляду з умовным выходам якраз і ёсць тым цыклічным працэсам, для якога ён створаны. Ollama адпавядае за запуск модэляў. За замовчаннем як генератор выкарыстоўваецца qwen3.5:9b, як крытык — deepseek-coder:6.7b, а як адпаведальны — llama3:8b, але невялікі файл JSON прызначае ролі, таму будзь-што, што вы завантажылі локальна, можа выступіць у якой-небудзь з іх ролей.
Рэзультатам являецца адна команда консолі — reflexion. Яна чытае вучальны файл, запускае цікл, выводзіць табель рэйтынгу для кожнага раунду і запісвае апрацоўаны Markdown на дыск. Весь працэс размешчаецца ў аднам файле на Python, які складаецца прыблізна з 360 ліній.
Структура складаецца з семі элементаў: стан рабочага працэсу і схема ацэнкі крытыка, вузол Генератора, вузел Крытыка, маршрутызатор, які вяршыць выбор между цыклам і завершэнням, вузол Аджудыкатора, схема з’ѐеднання вузлаў і, нарэшце, час адработкі з канфігурацыяй і чысткай выходных дадзеных.
Настройка
Код знаходзіцца ў рэпазітарыі пад назвай local-reflexion-agent як адна пакетацыя — src/reflexion/main.py, якая выканана як скрыпт консолі праз pyproject.toml. Клонаваце яго і інсталюйце ў рэжыме рэдагавання:
git clone https://github.com/jfjensen/local-reflexion-agent.git
cd local-reflexion-agent
pip install -e .
Гэта запоўнюе langgraph, langchain-core, langchain-community, langchain-ollama і pydantic, а таксама рэгіструе скрыпт для консолі пад назвай reflexion. Пасля чаго трэба паказаць, што Ollama працуе локальна, і завантажыць тры модэлі, названыя ў config.json:
ollama pull qwen3.5:9b
ollama pull deepseek-coder:6.7b
ollama pull llama3:8b
Разныя модэлі — гаразд. Ролі прызначаюцца ў файле config.json, які знаходзіцца ў корні рэпазітарыя, і зноў практычна расказана пра гэта ў раздзеле пра выконанне ніжэй. У рэпазітарыя ў каталогу input/ ёсць тры маленькія прыкладныя файлы, таму пайплайн можа запусціцца адразу:
reflexion --input input/source_function_a.py --output output/result.md --config config.json
Частка 1: Дзяржава і схема крытыка
Спачатку выкорыстоўваюцься два структуры дадзеных. Адна з іх візначае, што крытык павінен вернуць, а другая — памяць, якая пераходзіць праз граф.
Рэзультат работы крытыка ёсць болей цікавым. Якшо запітаць местную мадэль размерам 6,7 мільярда пра оцэнку, яна весела адпавядае параграфам прозы. Натоўпаюцься цыфры плюс адна практычная рэшыць, таму схема Pydantic змушвае мадэль запалняць фіксаваныя поля:
class DocEvaluationSchema(BaseModel):
"""Pydantic schema used to force a structured JSON schema constraint out of our local critic model.
Deliberately contains only judgment fields: the overall average and the pass/fail decision are computed in code."""
technical_accuracy: int = Field(description="Score from 0-100 on accuracy.")
completeness: int = Field(description="Score from 0-100 covering parameter definitions.")
single_critical_fix: str = Field(description="The single absolute highest priority correction required.")
Калькіляванні працэспаду схемы:
- Поле
single_critical_fixявляецца сэрцам ціклу. У зменшэнне з складу спіса всіх проблем крытык называе адну самую важлівую палатку. Невялікі генератор набаго лепей справляецца з задачай «палатку гэтае адна рашчынка» чым з масою сумешаных адзінакоў. - Тое, што не ўключанае, мае такі ж значэнне, як і тое, што ўключанае. У болей ранніх версіях таксама прасілі мадэль падаць загальны процэнт і флаг «прыйнята/непрыйнята», што віявілася памылкай, якая опісана пад роздзелом «Крытык». Корача кажучы: ніколі не прасіце мадэль выконваць арыфметычныя расчыткі чыстаўанне пороговых значэнняў, якія Python можа выкарыстоўваць у адной лініі. Схема мае толькі тыя адзінакі, якія даслівна неабходны модэлю, тады калі жоўтак «Крытык» выкарыстоўваецца для сярэдньявання і самага прыняцця рашчынкі «прыйнята/непрыйнята» у простым кодзе.
Field проста дэкорацыяй. Яны прабрачываюцца да модэлі ў рамках абмежэння JSON schema і функцыонуюць як маленькія підказкі.Далей ідзе стан. LangGraph перадае аб’ект стану з вузла на вузел, а кожны вузел адпавядае частковым апдэйтом гэтага стану. TypedDict задае яго структуру:
def append_scores(old: List[int], new: List[int]) -> List[int]:
return old + new
class AgentWorkflowState(TypedDict):
"""Memory tracking state for the heterogeneous local graph layout."""
source_code: str
generated_markdown: str
current_score: int
score_history: Annotated[List[int], append_scores]
required_fix: Optional[str]
iteration_count: int
is_approved: bool
final_adjudicated_markdown: Optional[str]
Едына непроста лінія — гэта score_history. Тып Annotated[List[int], append_scores] прымушуе LangGraph об’еднуваты оновлення гэтага поля за допомогою функціі append_scores, а не перэзапісуваты яго. Такім чыном кожэн показнік, повернутый крытыком, додаецца до історыі. Напрыкінці працэсу створуецца повна крыва оцінок за всі етапі рэдагування, што ёсць найбільш інформатывною даніною, яку віводыць сістэма.
Частына 2: Вузол Generator
Вузол LangGraph — гэта просто функція: у неі входыць стан, а віходыць словнік з оновленнямі. У Generator ёсць два рэжымі. Під час першого проходу створуецца чэрнетка документаціі непасрэдна з коду. Кожэн наступній прохід вносіць корэктыві до папярэдняй чэрнеткі відповідно до пропозіцій крытыка.
def content_generator_node(state: AgentWorkflowState) -> dict:
"""The Generator node: Focuses entirely on drafting/editing documentation text layout."""
current_iteration = state.get("iteration_count", 0)
target_model = MODEL_REGISTRY["GENERATOR"]
# ... console logging trimmed, see the repo for the full version
llm = ChatOllama(model=target_model, temperature=0.7)
if not state.get("generated_markdown"):
system_prompt = "You are an expert technical writer. Convert raw code snippets into clean, comprehensive API markdown documentation."
user_prompt = f"Generate Markdown documentation for the following source code:\n\n{state['source_code']}"
else:
system_prompt = "You are a precise editor rewriting technical documentation to fix a structural flaw."
user_prompt = (
f"Your previous documentation draft was scored {state['current_score']}/100.\n"
f"You must completely rewrite the documentation to fix this specific issue: {state['required_fix']}\n\n"
f"Original Code Context:\n{state['source_code']}\n\n"
f"Previous Draft:\n{state['generated_markdown']}"
)
messages = [SystemMessage(content=system_prompt), HumanMessage(content=user_prompt)]
response = llm.invoke(messages)
return {
"generated_markdown": response.content,
"iteration_count": current_iteration + 1
}
Што робіць код:
Выбір рэжыму. Калі generated_markdown пусты, значыць, гэта першы раунд, тады мадэль выступае як тэхнічны пісьменнік, працюючы з первісным імпульсам. Калі ужо ёсць чэрнавка, мадэль працюе як рэдагуўальнік і отрымае пярэдній бал, адзін прыказ крытыка, первісны імпульс і свою сябе пярэднюю чэрнавку.
Запрос на перэработку. Паведамленне модэлі пра ёе пярэдні бал — гэта просты трук, які работае. Конкрэтна фраза „Вы павысілі бал да 78/100“ падштовхуе яе зменіць тэкст, а не проста перафразаваць яго. Інструкцыя вымагае повнай перапісвы, каб выправіць адзін конкрэтны прынт, і такія часткавыя інструкцыі чудова падходзяць для малых локальных модэляў.
Тэмпература. Генератор працуе з параметрам temperature=0.7, што дае чэрнавкам певную разнаўтнасць між раундамі. Крытык, як будзе показана далей, працуе з тэмпературой 0.
Значэнне, якое вяртаецца. Вяртаюцца толькі змененыя поля. LangGraph з’еднае іх у стан, а iteration_count зрастае на адзін за кожны раунд генеравання.
Частка 3: Вузол Critic
Гэта іншая мадэль — за замовчаннем deepseek-coder:6.7b, і для таго, каб правільна промпт была створана, знадобілася большая колькасць спроб, чым для будзь-каго іншага элемента ў сістэме.
def content_critic_node(state: AgentWorkflowState) -> dict:
"""The Critic node: Cross-checks generator output using a different model with strict JSON formatting."""
target_model = MODEL_REGISTRY.get("CRITIC")
llm = ChatOllama(model=target_model, temperature=0.0)
structured_llm = llm.with_structured_output(DocEvaluationSchema)
system_prompt = (
f"You are a meticulous QA inspector evaluating API documentation against its source code. "
f"You are grading the DOCUMENTATION, not the source code. Never deduct points because the source code "
f"itself could be designed better, and never ask for the source code to be changed. "
f"Score by deduction: start from 100 and subtract points only for concrete errors you can verify by "
f"pointing at specific documentation text that contradicts the source code. "
f"Check that every 'raise' statement in the source is documented with its exact exception type and its "
f"exact quoted error message: a missing, denied, or misquoted exception is a major deduction. "
f"Recompute every numeric value shown in the documentation's examples against what the source code would "
f"actually return: a wrong number is a major deduction. "
f"If you find no verifiable errors, score 100. "
f"The single_critical_fix must point at the documentation text to change, not at the source code."
)
user_prompt = (
f"Evaluate the following markdown documentation relative to its raw source code.\n\n"
f"Raw Source Code:\n{state['source_code']}\n\n"
f"Generated Markdown Document:\n{state['generated_markdown']}"
)
evaluation: DocEvaluationSchema = structured_llm.invoke([
SystemMessage(content=system_prompt),
HumanMessage(content=user_prompt)
])
# The model provides the judgment scores; the arithmetic and the pass/fail decision are done here in code.
overall_score = round((evaluation.technical_accuracy + evaluation.completeness) / 2)
is_approved = overall_score >= QUALITY_SCORE_BAR
required_fix = sanitize_critical_fix(evaluation.single_critical_fix)
return {
"current_score": overall_score,
"score_history": [overall_score],
"required_fix": required_fix,
"is_approved": is_approved
}
Структураваны выхад. Самае важная лінія — llm.with_structured_output(DocEvaluationSchema). Яна обмежвае працю мадэлі так, каб у выходзе быў абмовлены об’ект DocEvaluationSchema, а не вольны тэкст. Гэта значыць, што няма паводку для парсінгу JSON, рэгулярных выразоў і логікі павторных спроб для некорэктнага выхаду. Працэс ацэнкі працюе з temperature=0.0, таму што ацэнкаўчык павінен быць дэтэрміністычны або якомога бліжэй да гэтага для локальнай мадэлі.
Як эвалюяўся запрос. Ён праходзіў тры версіі, і гісторыя ўжо самае ціннае выклеканне тут.
- У першай версіі модэлю ввічліва прасілі аналізаваць дакументацыю і ставіць яй оценку. Рэзультатам былі ввічлівыя оценкі у дыяпазоне 80–90 паштоў для дакументаў, якія мелі выдуманыя паведамленні пра аберэння, таму цыкл завершыўся занадта рана.
Не включайце арыметыку ў модэль. У першай схеме модэль должны быў выдаваць загальны процэнт і сам булевы значэння «прымана/не прымана», а рэзультаты былі няўпорадкованыя. Аднаго разу эксперт прызначыў тэхнічную точнасць 100, а полнасць 100, а пасля заявіў пра «чыстую сярэжку» роўную адносна 95, што было проста прагаматай, згаданай у задачы, а не сярэжкай чаго-небудзь іншага. Іншы раз ён прызначыў оцэнку 97 па прагаматай у 95 за чатыры раунды паспало, пры тым кожны раз зазначаючы, што робат не прымана. Усе чатыры спробы былі марнаваны на праекты, якія вэсна ўжо падпадалі пад крытэрыя, таму што маршрутазысквальнік верыў у булева значэння, а журнал спакойна зазначаў, што 97 не дасягае 95.
Сёньдзе модэль дае толькі два падскоры. Тры рэчыны Python адрабатвае ўсё іншае: вылічае сярэднюю значэнне, яе параўнанае з QUALITY_SCORE_BAR, і задаеся флаг, які выкарыстоўваецца раутэрам. Параўнанне чысла з прагамай не ўключае суджэння, і все, што не ўключае суджэння, не трэба даваць у веданне модэлю.
Захад sanitize_critical_fix. У тым жа працаванні на чатыры раунды адзін з крытыкаў у аднам з раундоў падаў кількіста каля кількох сотэй павтароўчых токэнаў ../, і гэтыя токэны былі вставлены слова за словам у наступны запіт генератора як проблема, якую трэба выправіць. Маленькія мадэлі, якія працуюць пад абмежэннямі структураванага выходу, часамі парадксальна себе ведуць. Шыба пераканальваецца, чы не ўжо выправленне ў спяцэйнім стане (адзін павтароўчы токэн або тэкст, складзены толькі з каля калькі разных сімвалаў), і, як такое адбуваецца, заменяе яго на універсальны запіт на паўтарную перапрацоўку дакументацыі па аднородным выкліканню. Гэта не ўжо такі складны спосаб, але ён не дазволяе непатрэбнаму матэрыялу вплываць на весь раунд. Усія функцыі ўсьмо туткі:
def sanitize_critical_fix(fix: Optional[str]) -> str:
"""Guards against degenerate critic feedback (empty strings or runaway token repetition)."""
stripped = (fix or "").strip()
words = stripped.split()
is_degenerate = (
not stripped
or len(set(stripped)) <= 4
or (len(words) > 5 and len(set(words)) <= 2)
)
if is_degenerate:
return "Re-verify the documentation line by line against the source code and correct the weakest section."
return stripped
Кожнага раунду оцінка повертаецца у вігляды спіску, што містыць одын елемент, а рэдзюсер стану, візначэній ранішэ, додае яго до історыі. Саме так накопічуецца крыва оцінок. Таксама відображаецца табліця оцінок раунду, якая показуе тэхнічну дакладнасць, повноту інформацыі, чыстый бал параўнано з показніком і відгукі крытыка. Ці даныя відаляюцца з віхідного тэксту, але залішаюцца у рэпазіторыі; спостэрэжэння за табліцею оцінок з раунду в раунд — гэта половіна задоволення.
Частына 4: Маршрутызатор
Генератор і крытык ужо готові, тому цяпер хтось мусіць вірышуваты, што будзе далі пасля кожноі оцінкі. У LangGraph цю функцію віконуе маршрутызаційна функція, якая чытае стан і повертае назву наступного вузла.
def execution_router(state: AgentWorkflowState) -> Literal["content_generator", "content_adjudicator"]:
"""Monitors workflow states and determines whether to repeat, loop, or step out to adjudication."""
if state["is_approved"]:
return "content_adjudicator"
if state["iteration_count"] >= MAX_ATTEMPTS:
return "content_adjudicator"
return "content_generator"
Зьвярненне да раутэра:
- У цыкле ёсць толькі два варыянты выходу: крытык схвалюе праект, альбо досягаецца ліміт спроб. У будзьму случае праект пераходзіць да Аджудыкатора; у всіх іншых случаях ён вяртаецца да Генератора.
- Этот ліміт заслуговае на большыя увагу, чым можа здавацца. З 95-бальным стандартам і строгім крытыком дзеяныя праекты ніколі не будуць схвалены, а без
MAX_ATTEMPTSцыкл будзе несканчатна вызываць Ollama. Чатыры спробы — гэта довільная, але працэйна цыфра, яю можна змяніць. - Тып вярнення
Literal— гэта не проста дакументацыя, адколі ён пасуе да карты маршрутацыі, заданай пад час створэння графа. - Як і у віпадку з іншымі вузламі, выведанне ў консоль адсекаецца з выдзялкі. У рэпазітарыі раутэр фіксуе колькасць спроб і історыю балаванняя перад кожным рашэннем.
Частка 5: Вузел Аджудыкатор
Ўніт, які выйшлі з цыклу – незалежна адзь тое, працаваў ён успешна чы розтрымаліся спробы – атрымаюць пасляпэўныя перагледы ад трэціго модэлю. Задача Аджудыкатара не ў тым, каб дадаць новы контэнт; яна выправляе суперсчыленні, якія з’явіліся пасля кожнага раунду пераглядоў, і адключае ўсё, што мае характэр дыялогу.
def content_adjudicator_node(state: AgentWorkflowState) -> dict:
"""The Adjudicator node: Runs outside the loop to resolve structural contradictions and finalize text."""
target_model = MODEL_REGISTRY["ADJUDICATOR"]
llm = ChatOllama(model=target_model, temperature=0.2)
system_prompt = (
"You are a senior technical documentation editor giving the final word on an API document "
"that has been through multiple revision rounds. Your job is to read the raw source code, "
"look at the latest draft, eliminate any conflicting statements, clean up semantic prose, and output the polished final markdown document. "
"Do not include any preamble, comments, conversational pleasantries, or explanations-return ONLY the markdown documentation."
)
user_prompt = (
f"Review and finalize this documentation template.\n\n"
f"Raw Source Reference Code:\n{state['source_code']}\n\n"
f"Latest Revision Draft:\n{state['generated_markdown']}\n\n"
f"Last Remaining Critic Concern:\n\"{state['required_fix']}\""
)
response = llm.invoke([SystemMessage(content=system_prompt), HumanMessage(content=user_prompt)])
return {"final_adjudicated_markdown": response.content}
Прычына існавання вузла. Пасля калькі раундаў «пэўнае перапісванне, каб выправіць X» праект можа мець внутршнія суперсчытанні: у адным раундзе додаецца твэрджэнне, а пазнейша версія яго заперачвае, не выкарыстоўваючы ёго для выдалення. Аджудыкатар атрымлеў вачоўны матэрыял, найновейшы праект і пасляпэўнюе застарэлую прытворку крытыка, а таксама інструкцыю падбіраць твэрджэнні, якія суперсчытаюцца. Гэта таксама служыць спосабам выйшчы для праектаў, якія так і не перасілі крокі атстэйты. У замяне на выдазванне неапранутаг чэтвёртаг прабавы, канвейер прынаймні дастае гарманізаваную версію.
Тэмпература. Ёна працуюе з параметрам temperature=0.2. Гэта нізкае значэнне, таму што галоўная задача — рэдагаванне, але не нуль, таму што ёй яшчэ трэба перапісваць тэкст.
Толькі Markdown. Местныя мадэлі любяць пачынаць з фразы "Звычайна! Ось ваша дакументацыя:" і заканчываць фразай "Спадзяюся, гэта дапаможа." У запіце забораняюцца як першы, так і другі варыянты. Лячна сама по сабе недастаткова, як паказвае раздзіл пра час выканання, таму наэтапна апрацоўка неабходна, каб усунуць все, што могла бы застацца.
Частка 6: Паўязка графа
Цяпер начынкі складаюцца ў ўсёе цэлае. Самэлькі LangGraph знаходзіць сваё месца самэлькі тут, калі ўвесь топалогія, уключаючы цыклы, можа быць запісаным у калькі ліній:
builder = StateGraph(AgentWorkflowState)
builder.add_node("content_generator", content_generator_node)
builder.add_node("content_critic", content_critic_node)
builder.add_node("content_adjudicator", content_adjudicator_node)
builder.set_entry_point("content_generator")
builder.add_edge("content_generator", "content_critic")
builder.add_conditional_edges(
"content_critic",
execution_router,
{
"content_generator": "content_generator",
"content_adjudicator": "content_adjudicator"
}
)
builder.add_edge("content_adjudicator", END)
agent_pipeline = builder.compile()
Вузлы і точка входу. Тры функцыі вузла зарэгістрованы пад своімі іменамі, прычым калі Генератор выступае як точка входу. Генератор завжды перадае задачу Крытыку, і гэта вялісць адбываецца без жадных умов.
Умовная вялісць. Функцыя add_conditional_edges спаявае рутэр з Крытыкам. Калі крытыка завершыцца, запускаецца рутэр, а яго адказ шукаецца ў табліцы супарабоцавання, каб знайсці наступны вузел. Гэта табліца стварае цыкл, таму што content_critic можа вярнуцца да content_generator, які зноў падае даныя для content_critic.
Кампіляванне. Выклік функцыі builder.compile() стварае канвейер, які можна запусціць, тады весь цыкл стае адным вызовам agent_pipeline.invoke(initial_state).
Кожны, хто пасля рукі підбіраў такой цыкл з выкладзенням while, ручным кераваннем станамі і лічыльнікам спроб, зверне ўвагу на тое, насколькі менш стала патрабовацься ведучыя кніга. Адзначэнне графа таксама даступнае для чытання у спосабе, якого ніколі не можа зробіць ручны цыкл.
Частка 7: Час выканання, настройкі і чыстка выходных даных
Паслягае функцыя run(), якая знаходзіцца за скрыптам консолі reflexion. Яна парсуе аргументы, завантажвае настройкі, запускае працэс апрацоўкі і чыстае выходныя даны. Большая частка ціх процэсаў, у тым ласкава працэс парсавання аргументаў, лепшая розумецца, пераглянуўшы рэпазітарый, але два аспекты заслуговуюць на увагу.
Спачатку прыходзяць настройкі. Файл config.json задае ролі модэляў і настройкі цыклу:
{
"model_registry": {
"GENERATOR": "qwen3.5:9b",
"CRITIC": "deepseek-coder:6.7b",
"ADJUDICATOR": "llama3:8b"
},
"quality_score_bar": 95,
"max_attempts": 4
}
Калі файл не існуець або немагчыма яго прачытаць, запускач выкорыстоўвае тыя ж стандартныя настройкі, таму працэс працы ўсё равно запускаецца. Стандартныя месцы для вхідных дадзеных, выходных дадзеных і налашоўкаў вылучаюцца абсалютна па вазе інсталаванага пакета; у разе інсталляцыі для рэдагавання гэтым месцам являецца корневы каталог рэпазітарыя. Таму запуск простага reflexion з будзь-якага каталога дазволяе знайсці файл config.json і папку input/ у рэпазітарыя, тады як параметры --input, --output і --config можаўна зменіць кожны з гэтых элементаў.
Другое – чыстка выходных дадзеных, якая стала неабходнаю праз работу з локальнымі мадэлямі:
raw_final_text = final_output.get("final_adjudicated_markdown", "")
clean_final_markdown = raw_final_text
# 1. Clean Preamble (Conversational text at the top)
if "#" in raw_final_text:
parts = raw_final_text.split("#", 1)
if len(parts[0].strip()) > 0 and len(parts[0].strip()) < 200:
clean_final_markdown = "#" + parts[1]
# 2. Clean Postscript (Conversational and meta review text at the bottom)
meta_prefixes = (
"note:", "note that", "i hope", "this concludes", "in conclusion",
"no formatting errors", "no contradictions", "no errors",
"let me know", "feel free", "please let me know"
)
meta_keywords = ("errors or contradictions", "contradictions were found", "errors were found")
lines = clean_final_markdown.rstrip().split("\n")
while lines:
last_line = lines[-1].strip()
if not last_line:
lines.pop()
continue
lowered = last_line.lower()
# Real content usually carries Markdown structure; meta chatter does not.
is_structural = lowered.startswith(("#", "|", "-", "*", ">", "`")) or lowered[0].isdigit()
is_meta = (not is_structural) and (
lowered.startswith(meta_prefixes) or any(k in lowered for k in meta_keywords)
)
if not is_meta:
break
lines.pop()
clean_final_markdown = "\n".join(lines).rstrip() + "\n"
Як гэта працюе:
- Перакантролька прадзея. Калі тэкст стоіць пры першай загаловцы
#і яго дужна 200 сімвалаў, гэта практычна завялік «Ёсць готавы дакумент» і ён выкарыстоўваецца як пачатак, а потым выкарыстоўваецца як частка дакумента. Калі тэкст дужы больш 200 сімвалаў, ён застаецца, бо можа якраз быць справжнім контэнтам. - Перакантролька паслядзейнасці. Код перагледвае тэкст з апошней лініі да першай, лінія за лініяй. Лінія выкарыстоўваецца як частка дакумента, калі ёй не паслужыць як мадэльны тэкст: напрыклад, застаўшыся вердыкт адзінакоўвання, падпіс, які начынаецца словамі «Прымечанне» або «Маю надзею», такімі як «Не было знайдзенаў ніякіх пакрысэнняў фарматавання або суперасункі». Усе гэта можа застацца ў канцы дакумента пасля адной тэставай перагледкі. Ускладненне гэтага працэсу тым, што ён адбываецца за дапамою ціклу, а не адной перакантролькі, значыць, калькі падпісоў можа знікнуць за адну перагледку.
Пасля чысткі фінальны фармат Markdown зберагаецца на шляху выходу. Пасля чаго консоль паказвае статыстыку адработкі, укладаючы ў сябе весь історыя балавання і колькасць разоў адбыўшыхся цыклаў.
Пробаўце
У рэпазітарыі ўключаныя тры прыклады вхідных дадзенняў: функцыя для фільтрацыі метрык, перакананне падпіса API і калькулятор складных відсоткаў. Документаванне першай з іх выглядае так:
reflexion --input input/source_function_a.py --output output/result.md --config config.json
З таго момента можна бачыць, як працюе цыкл. Кожны раз Генератор анансавае сябе, Крытык выдрукавае табліцу з рэйтынгамі, а Рутэр паведамляе, чы робіцца цыклізацыя чы выход з яго.
Рэзультаты працы ўсіх трох вхідных дадзеных знаходзяцца ў папцы output/, таму продукты пайплайна можна пераглядзець без неабяжнай ўваходу коду: апрантныя табліцы параметраў, апісанні значэнняў, якія вяртаюцца, і табліцы выкліканых асаблівэнняў з ўсіма ўсунутымі тэкстамі. У двух з трох файлаў ёсць невялікіія памылкі, опісаныя ніжэй — заявлены выхід, які не збігаецца з тым, што вяртае код, і прыклад, у яком выклікаецца асаблівэнне там, дзе трэба было вярнуць значэнне. Іх залічылі намерна, таму ўсуненне іх ёсць непаслабным задачэм.
Калі ўсё йдзе не так
Іншыя разы таксама выходзіць на адміністрацыю, і спіс ніжэй падае з неудач, зафіксаваных пад час тэставання. У аднам з тэстаў у дасягліннях быў указан некорэктны тып асключэння і вымышленая паведамленне пра адміністрацыю, хоця сам код выклікае TypeError з конкрэтным строкам. У іншам тэсце стаўілася тверджэнне, што нічога не выклікаецца. У табліцы історыі версій быў прадстаўлен вымышлены радок «0.1 пачатковая версія» з датой «TBD», якіх ніяк не існуе ў самым кодзе. У аднам з прыкладаў быў паказана ступеня выдання блізу 0.86, хоць правільны адпаведнік — 0.667. А ў аднам адносна якісным дасяглінні на канцы была супэльніца самага крытыка: «Не было знайдзенаў ніякіх памылак у фарматаванні чы суперсчыненняў».
Потым буў той віпадак, калі за чатыры раунды було набрана 97 балоў проты 95, праз тое, што сістэма настойчыва стверджвала, што праект не готавы. Адзін раунд зворотнага зв’язку ператварыўся на сотні токенав ../. Калі ж крытарскі прыказ узяў занадта агрэсівны характэр, ён чатыры раунды пасляроўна даваў 0 з 100 балоў за пераважна правільны праект, абараняючы такі рэйтынг явна неправдзівымі тверджэннямі пра выхідны матэрыял. Крытар не ёсць фіксаваным суддзёй і не калькуляторам, і ён падсілляе будзь-які настроў, які дае йому прыказ.
На ўдачу, неякасці скупляюцца па групамі, і кожная група мае спецыяльныя заходы змягчэння ў кодзе. Неякасці кантэнту, такія як некарэктныя аберанцыі і памылкі ў арытмэтычных вырахунках у документах, адмаўляюцца за дапамогою пераконтральных процедурао, названых у прамтаві крытыка. Неякасці калібрацыі (занадто лёгкія атэстаты з однай стороны, надмерна нізкія значэння з іншай) адмаўляюцца за дапамогою процедуры расчытання балава, якая базуецца на выніканнях, а таксама шляхом перанесення расчытка сярэдньага значэння і прагу з мадэлі ў код на Python кроеку крытыка. Неякасці выходных даных, падпісаў і засталых вердыктаў адрасавання, адмаўляюцца за дапамогою процедуры перагледу пасля выдання рэзультатаў, а неякасці зворотнага зв’язку — за дапамогою механізма санітарнай пераконтролі.
Нічыя з гэтых мер не робіць цыкл абсалютна надзеямым, адказаў на запіт ёсць толькі просьбай, а не доказам. Калі пасля всіх гэтых заходаў рэйтынгі все ўсё зміняюцца, найбольша сіла лежыць у самай модэлі, а не ў запіце. Крытык мае найважчую працу ў рэўістры, а замена элемента CRITIC у файле config.json на моцнейшую універсальную модэлю — гэта змяна, якая можа быць адбыта за адну лінію без неабходнасці пісаць код.
Цыкл надзеямо даёт значна лепшы дакумент, чым запіт, даный толькі адной разоў тому ж генератару, а історыя рэйтынгаў даёць чысты адказ пра тое, як гэта было досягнуто. Калі падчас запускаў продовжвае быць перакрытая межа прабоў, варта налаштаваць два параметры: quality_score_bar, які можа быць ніжэй 95, і max_attempts, які можа быць вышэй 4.
Аднаўленне ўсьго разам
Рэзультатам ёсць пайплайн у адном файле, які складаецца з прыблізна 360 ліній і:
- Саюзны стан, а таксама структураваныя схемы ацэнкі для крытыка; вырачунакі сярэгаў і рашэння пра адзначэнне якіснасці выкарыстоўваюцца ў Python, а не самым модэлем.
- Чорнавік, напісаны адным местным модэлем і перакантраваны іншым, тады кожны модэль не ацэнівае сябе самога.
- Рутэр, які вяртае чорнавікі на дальнейшую праверку, пакуль не будзе досягнута патрэбная якіснасць або не заканчыцца спробы.
- Выкарыстоўвае трэці модэль як фінальнага суддзю, каб рашыць працоўныя суперсечанні, якія засталіся пасля раундаў праверкі.
- Удаляе прадзея і заканчыцця дыялогу з фінальнага Markdown-тэксту пры яго запісе на дыск.
- Наладжэнні зберагаюцца выключна ў маленькам файле
config.json; таму для змены модэляў аб адпускання цыклу не трэба правяць жадных змян у кодзе.
Есць можлівасць яго расшырэння. Крытык можа оцаніць кальколька вімера рубрыкі, пры чым рутэр выберае найслабейшы з яных для практычнага выправлення, замест адзінаго толькі крытычнага рашэння. Чатверты вузол могае запускаць прыклады документаванага коду і вяртаць рэальныя памылкі назад у цыкл, выяўляючы тые халюцынатываныя тверджэння, якія крытык працягвае перагледзець. Або тая ж структура з Генератаром, Крытыкам і Аджудыкатарам можа быць выкарыстоўаная для зовсім іншага заведамства, такога як пераклад чы стварэнне тэстаў, адколі нічога ў графе не ёсць спецыфічным для документаўвання.
У гэтым случае практычна цянькавасць фрэймворкаў: яны становяцца корыстнымі тады, калі рабочы працэс мае справжнія цыклы, галузі і спакульнае становішча, і гэты пайплайн падходзіць пад такыя апісанні; LangGraph абдалася тымі часткамі, якія ніхто не хоча рэалізаваць вручную, а саме з’еднанням станоў, умовным направленнем трафіку і самым цыклам. Спачатку стварэнне лёгкіх версій дапамагае з’ясавіць, якія часткі фрэймворка выпрацоўваюць рэальную работу, і такі парадак варта рэкамендаваць.