🇬🇧 English
LangGraph is not an LLM library. It is a small state machine: you declare what the state looks like, write nodes that change it, and draw edges between them.
The first two files here have no model in them at all, on purpose. Everything people find confusing about agent frameworks is in those two files.
Then the model arrives, and its only job is to fill in state that an edge reads.
🇹🇭 ไทย
LangGraph ไม่ใช่ไลบรารี LLM มันคือเครื่องสถานะเล็ก ๆ คุณประกาศว่าสถานะหน้าตาเป็นยังไง เขียนโหนดที่เปลี่ยนมัน แล้วลากเส้นเชื่อมระหว่างโหนด
สองไฟล์แรกที่นี่ไม่มีโมเดลอยู่เลย และตั้งใจให้เป็นแบบนั้น ทุกอย่างที่คนงงเกี่ยวกับเฟรมเวิร์กเอเจนต์อยู่ในสองไฟล์นั้น
จากนั้นโมเดลค่อยเข้ามา และหน้าที่เดียวของมันคือเติมสถานะที่เส้นเชื่อมจะอ่าน
🇨🇳 中文
LangGraph 不是一个 LLM 库。它是一台小小的状态机:你声明状态长什么样,写一些改变它的节点,然后在节点之间连边。
这里头两个文件里根本没有模型,这是故意的。大家对智能体框架感到困惑的一切,都在那两个文件里。
然后模型才登场,而它唯一的工作,是把某条边要读的状态填上。
| A chainเชน链 | A graphกราฟ图 | |
|---|---|---|
| Shapeรูปร่าง形状 | a line: A then B then Cเส้นตรง A แล้ว B แล้ว C一条线:A 然后 B 然后 C | edges you drew, including ones that go backเส้นเชื่อมที่คุณลากเอง รวมถึงเส้นที่ย้อนกลับ你自己连的边,包括往回连的 |
| Branchingการแตกกิ่ง分支 | an if statement inside a stepคำสั่ง if ที่อยู่ในขั้นตอน某一步里的一个 if | a conditional edge, visible in the diagramเส้นเชื่อมมีเงื่อนไข มองเห็นได้ในแผนภาพ一条条件边,在图上看得见 |
| Stopping halfwayหยุดกลางทาง中途停下 | start again from the topเริ่มใหม่ตั้งแต่ต้น从头再来一遍 | a checkpoint, resumed by thread idเช็กพอยต์ ทำต่อด้วย thread id一个检查点,用 thread id 接着跑 |
| Two agentsสองเอเจนต์两个智能体 | a framework concept you adoptแนวคิดของเฟรมเวิร์กที่คุณต้องรับมาใช้一个你得接受的框架概念 | two nodes and a routing functionสองโหนดกับฟังก์ชันจัดเส้นทาง两个节点加一个路由函数 |
| Measured hereวัดได้ที่นี่这里实测 | 1 call, 134 tokens, 6.1 s1 ครั้ง 134 โทเคน 6.1 วิ1 次调用,134 token,6.1 秒 | 4 calls, 408 tokens, 3.9 s4 ครั้ง 408 โทเคน 3.9 วิ4 次调用,408 token,3.9 秒 |
🧰 Step 0 — Install it, and point it at a model · ขั้นที่ 0 — ติดตั้ง แล้วชี้ไปที่โมเดล · 第 0 步 —— 装好它,并指向一个模型
💻 INSTALL
python -m venv C:\lg\venv # Windows. Keep the path short.
C:\lg\venv\Scripts\activate
python3 -m venv ~/lg/venv # macOS / Linux
source ~/lg/venv/bin/activate
pip install langgraph langchain-ollama
python -c "import importlib.metadata as m; print(m.version('langgraph'))"
# this page was written against langgraph 1.2.12 and langchain-ollama 1.1.0
💻 A MODEL TO TALK TO, FREE AND LOCAL
# Install Ollama from https://ollama.com, then pull the model:
ollama pull qwen2.5:14b # 9 GB. llama3.1:8b works too, and is smaller.
# Check it answers:
curl http://127.0.0.1:11434/api/tags
# Every file below reads this, so a model on another machine needs only:
export OLLAMA_HOST=http://192.168.1.50:11434 # macOS / Linux
$env:OLLAMA_HOST = "http://192.168.1.50:11434" # Windows PowerShell
llama3.1:8b and expect the classifier to slip.
ทุกอย่างที่นี่รันกับ qwen2.5:14b บน DGX Spark ผ่าน LAN โมเดลที่เล็กกว่าจะทำตามคำสั่งจัดเส้นทางได้แม่นยำน้อยลง ซึ่งทำให้ขั้นที่ 3 ดูแย่ลง แต่ขั้นที่ 4 จะเหมือนเดิม เพราะขั้นที่ 4 ว่าด้วยการไม่เชื่อโมเดลตั้งแต่แรกอยู่แล้ว ถ้าคุณไม่มี GPU ให้ใช้ llama3.1:8b และเตรียมใจว่าตัวจำแนกจะพลาดบ้าง
这里的一切都是在局域网内一台 DGX Spark 上的 qwen2.5:14b 上跑的。更小的模型执行路由指令会更不可靠,这会让第 3 步看起来更糟,而第 4 步看起来一样——因为第 4 步讲的本来就是别去信模型。如果你没有 GPU,用 llama3.1:8b,并且预期分类器会出错。
⚙️ Tutorial 1 — A graph is a state machine · บทที่ 1 — กราฟคือเครื่องสถานะ · 教程 1 —— 图就是状态机
🇬🇧 English
Two files, no model in either. If you understand these, the agent parts later are only prompts on top.
🇹🇭 ไทย
สองไฟล์ ไม่มีโมเดลทั้งคู่ ถ้าคุณเข้าใจสองอันนี้ ส่วนเอเจนต์ที่ตามมาก็เป็นแค่พรอมป์ที่วางทับอยู่ข้างบน
🇨🇳 中文
两个文件,都没有模型。如果你懂了这两个,后面那些智能体的部分不过是叠在上面的提示词而已。
1 One node, one piece of state · หนึ่งโหนด หนึ่งสถานะ · 一个节点,一份状态
📄 graph1.py
"""The smallest graph there is: one node, one piece of state."""
from typing import TypedDict
from langgraph.graph import START, END, StateGraph
# The state is the only thing nodes can see or change. Declaring it as a
# TypedDict is not decoration: LangGraph reads these keys to work out what
# a node is allowed to write, so the class is required, not a style choice.
class State(TypedDict):
topic: str
note: str
# A node is a plain function. It takes the whole state and returns only the
# keys it wants to change.
def write_note(state: State) -> dict:
return {"note": "Chiang Mai is the topic: " + state["topic"]}
builder = StateGraph(State)
builder.add_node("write_note", write_note)
builder.add_edge(START, "write_note")
builder.add_edge("write_note", END)
graph = builder.compile()
result = graph.invoke({"topic": "khao soi", "note": ""})
print("topic:", result["topic"])
print("note :", result["note"])
📋 WHAT IT PRINTS
topic: khao soi
note : Chiang Mai is the topic: khao soi
| The lineบรรทัด这一行 | What it doesมันทำอะไร它在做什么 |
|---|---|
class State(TypedDict) | The only channel between nodes. A node cannot pass an argument to the next node; it writes to the state and the next node reads it.ช่องทางเดียวระหว่างโหนด โหนดหนึ่งส่งอาร์กิวเมนต์ให้โหนดถัดไปไม่ได้ มันเขียนลงสถานะแล้วโหนดถัดไปอ่านเอา节点之间唯一的通道。一个节点没法给下一个节点传参数;它写进状态,下一个节点再读出来。 |
def write_note(state) | A node is an ordinary function. Nothing is registered, decorated or inherited — that is the whole interface.โหนดคือฟังก์ชันธรรมดา ไม่มีการลงทะเบียน ไม่มีเดคอเรเตอร์ ไม่มีการสืบทอด นั่นคืออินเทอร์เฟซทั้งหมด节点就是一个普通函数。不用注册、不用装饰器、不用继承——接口就这些。 |
return {"note": ...} | Return only what changed. Anything you leave out keeps its old value, which is why topic is still in the result.คืนเฉพาะสิ่งที่เปลี่ยน อะไรที่คุณไม่ใส่ก็คงค่าเดิมไว้ ซึ่งเป็นเหตุผลที่ topic ยังอยู่ในผลลัพธ์只返回改动的部分。你没写的东西保持原值,这就是为什么结果里 topic 还在。 |
add_edge(START, ...) | START and END are the two built-in nodes. Forget the START edge and compiling fails rather than running an empty graph.START และ END คือโหนดสำเร็จรูปสองตัว ถ้าลืมเส้นเชื่อมจาก START การคอมไพล์จะล้มเหลว แทนที่จะรันกราฟเปล่าSTART 和 END 是两个内置节点。忘了连 START 那条边,编译就会失败,而不是跑一个空图。 |
builder.compile() | Validates the shape once and hands back something runnable. Build it at import time, not inside your request handler.ตรวจรูปร่างครั้งเดียวแล้วคืนของที่รันได้กลับมา ให้สร้างตอน import ไม่ใช่ในตัวจัดการรีเควสต์把图的形状校验一次,交回一个可运行的东西。在 import 时构建,不要放在请求处理函数里。 |
- Delete the
notekey fromStateand run it. The error names the key, which is the check the TypedDict is buying you. - Make
write_notereturn{"topic": "something else"}as well, and watch a node rewrite its own input.
- ลบคีย์
noteออกจากStateแล้วรัน ข้อผิดพลาดจะระบุชื่อคีย์ ซึ่งคือการตรวจสอบที่ TypedDict ให้คุณ - ให้
write_noteคืน{"topic": "something else"}ด้วย แล้วดูโหนดเขียนทับอินพุตของตัวเอง
- 把
note这个键从State里删掉再跑。报错会点名那个键,这正是 TypedDict 给你买到的检查。 - 让
write_note也返回{"topic": "something else"},看一个节点改写自己的输入。
2 The write that silently disappears · การเขียนที่หายไปเงียบ ๆ · 悄悄消失的那次写入
📄 graph2.py
"""Two nodes in a row, and the one rule that surprises everybody:
a node's return value is merged into the state, and by default merging
a key means replacing it.
"""
from operator import add
from typing import Annotated, TypedDict
from langgraph.graph import START, END, StateGraph
class Replaces(TypedDict):
steps: list[str]
# Annotated[..., add] tells LangGraph to combine the old value and the new
# one with add() instead of overwriting. For a list, add() is concatenation.
class Appends(TypedDict):
steps: Annotated[list[str], add]
def chop(state) -> dict:
return {"steps": ["chop the herbs"]}
def simmer(state) -> dict:
return {"steps": ["simmer the broth"]}
def build(state_class):
builder = StateGraph(state_class)
builder.add_node("chop", chop)
builder.add_node("simmer", simmer)
builder.add_edge(START, "chop")
builder.add_edge("chop", "simmer")
builder.add_edge("simmer", END)
return builder.compile()
without_reducer = build(Replaces).invoke({"steps": []})
with_reducer = build(Appends).invoke({"steps": []})
print("no reducer:", without_reducer["steps"])
print("with add :", with_reducer["steps"])
📋 WHAT IT PRINTS
no reducer: ['simmer the broth']
with add : ['chop the herbs', 'simmer the broth']
steps, something has to decide what the merged value is — and the framework refuses to guess. The reducer is you answering that question in advance. operator.add is the usual answer for lists; for a running total it would be the same function doing arithmetic, and for "last one wins" it is the default you already have.
LangGraph รันโหนดขนานกันได้ ถ้าสองโหนดเสร็จพร้อมกันและเขียน steps ทั้งคู่ ต้องมีอะไรสักอย่างตัดสินว่าค่าที่รวมแล้วคืออะไร และเฟรมเวิร์กปฏิเสธที่จะเดา reducer คือคำตอบที่คุณเตรียมไว้ล่วงหน้า operator.add คือคำตอบปกติสำหรับลิสต์ ส่วนถ้าเป็นผลรวมสะสมก็เป็นฟังก์ชันเดียวกันที่ทำเลขคณิต และถ้าเป็นแบบ "อันหลังชนะ" ก็คือค่าเริ่มต้นที่คุณมีอยู่แล้ว
LangGraph 可以并行跑节点。如果两个节点同时完成、又都写 steps,就必须有东西来决定合并后的值是什么——而框架拒绝去猜。reducer 就是你提前把这个问题回答掉。对列表来说 operator.add 是常规答案;如果是累加求和,就是同一个函数在做算术;而「后写的赢」就是你本来就有的默认行为。
- Add a third node and give it to both graphs. The gap between the two outputs widens by exactly one.
- Swap
addfor a function of your own,lambda old, new: new + old, and watch the order reverse.
- เพิ่มโหนดที่สามแล้วใส่ให้กราฟทั้งสอง ช่องว่างระหว่างสองเอาต์พุตจะกว้างขึ้นพอดีหนึ่งรายการ
- เปลี่ยน
addเป็นฟังก์ชันของคุณเองlambda old, new: new + oldแล้วดูลำดับกลับด้าน
- 加第三个节点,两个图都加上。两份输出之间的差距正好扩大一项。
- 把
add换成你自己的函数lambda old, new: new + old,看顺序反过来。
🔀 Tutorial 2 — Letting the model steer · บทที่ 2 — ให้โมเดลเป็นคนบังคับทิศ · 教程 2 —— 让模型来掌方向
🇬🇧 English
The model never touches the graph. It writes a word into the state, and a function you wrote decides what that word means.
🇹🇭 ไทย
โมเดลไม่เคยแตะกราฟเลย มันเขียนคำหนึ่งลงในสถานะ แล้วฟังก์ชันที่คุณเขียนเป็นคนตัดสินว่าคำนั้นแปลว่าอะไร
🇨🇳 中文
模型从不碰这张图。它往状态里写一个词,然后由你写的函数来决定那个词是什么意思。
3 The model picks the branch · โมเดลเลือกกิ่ง · 模型来挑分支
📄 router.py
"""Let the model choose the branch.
A conditional edge is a function that looks at the state and returns the
name of the next node. Here the model fills in the state that function
reads, so the model is steering without ever touching the graph.
"""
import os
from typing import TypedDict
from langchain_ollama import ChatOllama
from langgraph.graph import START, END, StateGraph
OLLAMA = os.environ.get("OLLAMA_HOST", "http://127.0.0.1:11434")
# temperature=0 because this call is a decision, not writing. The same
# message should take the same branch every time you run it.
llm = ChatOllama(model="qwen2.5:14b", base_url=OLLAMA, temperature=0)
KINDS = ["booking", "complaint", "other"]
class State(TypedDict):
message: str
kind: str
reply: str
def classify(state: State) -> dict:
prompt = (
"Classify this guesthouse message as exactly one word, "
"either booking, complaint or other. Reply with the word alone.\n\n"
+ state["message"]
)
raw = llm.invoke(prompt).content.strip().lower()
# The model returns a sentence whenever it feels like it, so narrow the
# text down to one of the three words we can actually route on.
kind = "other"
for candidate in KINDS:
if candidate in raw:
kind = candidate
break
return {"kind": kind}
def route(state: State) -> str:
return state["kind"]
def handle_booking(state: State) -> dict:
return {"reply": "Checking the calendar for those dates."}
def handle_complaint(state: State) -> dict:
return {"reply": "Sorry about that. The manager will call you today."}
def handle_other(state: State) -> dict:
return {"reply": "Thanks for writing. Someone will read this shortly."}
builder = StateGraph(State)
builder.add_node("classify", classify)
builder.add_node("booking", handle_booking)
builder.add_node("complaint", handle_complaint)
builder.add_node("other", handle_other)
builder.add_edge(START, "classify")
# The dict maps what route() returns to the node that should run next.
builder.add_conditional_edges(
"classify",
route,
{"booking": "booking", "complaint": "complaint", "other": "other"},
)
for node in ["booking", "complaint", "other"]:
builder.add_edge(node, END)
graph = builder.compile()
messages = [
"Do you have a room free on the 14th of March for two people?",
"The shower in room 3 has been cold for two days now.",
"What time does the night market start?",
]
for message in messages:
result = graph.invoke({"message": message, "kind": "", "reply": ""})
print(f"[{result['kind']:<9}] {result['reply']}")
📋 THREE MESSAGES, THREE BRANCHES
[booking ] Checking the calendar for those dates.
[complaint] Sorry about that. The manager will call you today.
[other ] Thanks for writing. Someone will read this shortly.
other rather than raising. A routing function that crashes on an unexpected word will take your whole graph with it, on a Tuesday, because someone wrote a longer sentence.
เมื่อถูกขอคำเดียว บางครั้งโมเดลตอบว่า booking บางครั้ง Booking. และบางครั้ง This looks like a booking request. ทั้งสามแบบเป็นคำตอบที่ถูกต้องของคำถามนั้น แต่มีแบบแรกเท่านั้นที่ใช้เป็นคีย์ของดิกชันนารีได้ ลูปนี้เปลี่ยนแบบไหนก็ได้ให้เป็นหนึ่งในสามสตริงที่รู้จัก และถอยไปที่ other แทนที่จะโยนข้อผิดพลาด ฟังก์ชันจัดเส้นทางที่พังเมื่อเจอคำที่ไม่คาดคิดจะลากกราฟทั้งอันของคุณล่มไปด้วย ในวันอังคารวันหนึ่ง เพราะมีคนเขียนประโยคยาวกว่าเดิม
当你要求只回一个词时,模型有时答 booking,有时答 Booking.,有时答 This looks like a booking request.。三种都是这个问题的正确答案,而只有第一种能当字典的键用。这个循环把其中任何一种都变成三个已知字符串之一,并且退回 other 而不是抛异常。一个遇到意外词就崩溃的路由函数,会在某个星期二把你整张图一起拖垮,就因为有人把句子写长了一点。
"bookings" fails at run time, halfway through a conversation.
คุณจะละมันไว้แล้วให้ค่าที่คืนมาเป็นชื่อโหนดตรง ๆ ก็ได้ แต่ให้เก็บไว้ เมื่อมีแมปปิงอยู่ ตัวคอมไพล์จะรู้ทุกกิ่งที่ตัวจัดเส้นทางของคุณไปได้ ดังนั้นการพิมพ์ผิดจะถูกจับตอนสร้างกราฟ ถ้าไม่มีมัน ตัวจัดเส้นทางที่คืนค่า "bookings" จะพังตอนรัน กลางบทสนทนา
你可以不写它,直接让返回值就是节点名。留着它。有了这张映射表,编译器就知道你的路由器能走的每一条分支,于是拼写错误在你构建图的时候就被抓住。没有它,一个返回 "bookings" 的路由器会在运行时、在对话进行到一半时失败。
- Add a fourth kind,
cancellation, toKINDSand the mapping, but forget to add the node. The compiler tells you before you ever run it. - Delete the narrowing loop and set
kind = raw. Then send a chatty message and read the traceback.
- เพิ่มชนิดที่สี่คือ
cancellationลงในKINDSและในแมปปิง แต่ลืมเพิ่มโหนด ตัวคอมไพล์จะบอกคุณก่อนที่คุณจะได้รันด้วยซ้ำ - ลบลูปที่ทำให้แคบลงออก แล้วตั้ง
kind = rawจากนั้นส่งข้อความยาว ๆ เข้าไปแล้วอ่าน traceback
- 往
KINDS和映射表里加第四种cancellation,但故意不加节点。编译器会在你还没跑之前就告诉你。 - 删掉那个收窄的循环,改成
kind = raw。然后发一条啰嗦的消息,读那段 traceback。
4 A loop, and why it must not trust the model · ลูป และเหตุผลที่ห้ามเชื่อโมเดล · 循环,以及为什么不能信模型
📄 loop.py
"""A loop is just an edge pointing backwards.
Two things this file is really about: how to write one, and why the exit
condition cannot be left to the model alone.
"""
import os
from typing import TypedDict
from langchain_ollama import ChatOllama
from langgraph.errors import GraphRecursionError
from langgraph.graph import START, END, StateGraph
OLLAMA = os.environ.get("OLLAMA_HOST", "http://127.0.0.1:11434")
llm = ChatOllama(model="qwen2.5:14b", base_url=OLLAMA, temperature=0)
MAX_ROUNDS = 3
RULE = "five words or fewer, and no colon"
def write(state) -> dict:
prompt = "Write one short tagline for: " + state["brief"]
if state["draft"]:
prompt += (
"\n\nYour previous attempt was: " + state["draft"]
+ "\nThe editor said: " + state["verdict"]
+ "\nWrite a better one. Reply with the tagline alone."
)
return {"draft": llm.invoke(prompt).content.strip(),
"rounds": state["rounds"] + 1}
def critique_by_model(state) -> dict:
prompt = (
"You are a strict editor. Here is a tagline: " + state["draft"]
+ "\n\nReply with exactly OK only if it is " + RULE
+ ". Otherwise reply with one short sentence saying what to fix."
)
return {"verdict": llm.invoke(prompt).content.strip()}
def critique_by_rule(state) -> dict:
# The rule is countable, so count it. A model asked to apply this
# rule agreed to it and then ignored it; len() cannot.
words = len(state["draft"].split())
if words <= 5 and ":" not in state["draft"]:
return {"verdict": "OK"}
return {"verdict": f"{words} words, colon present: {':' in state['draft']}"}
def good_enough(state) -> str:
# Two ways out, and the second is the one that saves you.
if state["verdict"].upper().startswith("OK"):
return "done"
if state["rounds"] >= MAX_ROUNDS:
return "done"
return "again"
class State(TypedDict):
brief: str
draft: str
verdict: str
rounds: int
def build(critic):
builder = StateGraph(State)
builder.add_node("write", write)
builder.add_node("critique", critic)
builder.add_edge(START, "write")
builder.add_edge("write", "critique")
builder.add_conditional_edges(
"critique", good_enough, {"again": "write", "done": END}
)
return builder.compile()
start = {"brief": "a quiet guesthouse in Chiang Mai",
"draft": "", "verdict": "", "rounds": 0}
for name, critic in [("model", critique_by_model), ("rule", critique_by_rule)]:
result = build(critic).invoke(start)
print(f"--- critic: {name}")
print(" rounds :", result["rounds"])
print(" draft :", result["draft"])
print(" verdict:", result["verdict"][:60])
# Every graph also has a hard stop. recursion_limit counts supersteps, not
# loops, and hitting it raises rather than returning a half-finished state.
print()
try:
build(critique_by_rule).invoke(start, {"recursion_limit": 2})
print("finished inside the limit")
except GraphRecursionError as error:
print("GraphRecursionError:", str(error)[:88])
📋 THE SAME BRIEF, TWO EDITORS
--- critic: model
rounds : 1
draft : "Escape to Serenity: Chiang Mai's Cozy Hideaway"
verdict: OK
--- critic: rule
rounds : 3
draft : Serenity Amidst Chiang Mai's Charm: Your Quiet Escape
verdict: 8 words, colon present: True
GraphRecursionError: Recursion limit of 2 reached without hitting a stop condition. You can increase the limi
len(draft.split()) <= 5 and ":" not in draft was rejected correctly every time. This is not a bad model or a bad prompt; it is what asking a language model to apply a countable rule looks like at this size.
บรรณาธิการถูกสั่งให้ตอบ OK เฉพาะกับสโลแกนที่มีห้าคำหรือน้อยกว่าและไม่มีเครื่องหมายทวิภาค มันเห็น "Escape to Serenity: Chiang Mai's Cozy Hideaway" ซึ่งมีหกคำและมีทวิภาค แล้วตอบว่า OK รอบเดียว อนุมัติ จบ ร่างเดียวกันที่ตัดสินด้วย len(draft.split()) <= 5 and ":" not in draft ถูกปฏิเสธอย่างถูกต้องทุกครั้ง นี่ไม่ใช่โมเดลแย่หรือพรอมป์แย่ แต่นี่คือหน้าตาของการขอให้โมเดลภาษาบังคับใช้กฎที่นับได้ ที่ขนาดเท่านี้
编辑被要求只对五个词以内、且没有冒号的标语回 OK。它看到的是 "Escape to Serenity: Chiang Mai's Cozy Hideaway"——六个词,还有一个冒号——然后回了 OK。一轮,通过,收工。同一份草稿交给 len(draft.split()) <= 5 and ":" not in draft 来判,每次都被正确地否决。这不是模型差,也不是提示词差;这就是在这个尺寸上,让语言模型去执行一条可数的规则会长成的样子。
MAX_ROUNDS. Build the counter first and treat the model's approval as a bonus, because a loop whose only exit is the model agreeing with you is a loop with no exit.
ดูบล็อกที่สอง สามรอบ และร่างสุดท้ายยังมีแปดคำและมีทวิภาคอยู่ดี บรรณาธิการแบบกฎปฏิเสธเรื่อย ๆ อย่างถูกต้อง และนักเขียนก็ยังทำให้สั้นลงไม่ได้สักที สิ่งที่จบลูปนั้นไม่ใช่ความสำเร็จ แต่คือ MAX_ROUNDS ให้สร้างตัวนับก่อน แล้วถือว่าการอนุมัติของโมเดลเป็นของแถม เพราะลูปที่มีทางออกทางเดียวคือโมเดลเห็นด้วยกับคุณ คือลูปที่ไม่มีทางออก
看第二块:三轮,最终草稿仍然是八个词,里面还有冒号。基于规则的编辑一直在正确地否决,而写手一直没能把它缩短。结束那个循环的不是成功,而是 MAX_ROUNDS。先把计数器写好,把模型的批准当成额外奖励,因为一个唯一出口是「模型同意你」的循环,就是一个没有出口的循环。
GraphRecursionError and you lose the partial result, which is exactly what you do not want at the end of an expensive run. Use it to catch the bug where a router sends every state back to the same node; use MAX_ROUNDS to express how many revisions you actually wanted.
ทุกกราฟมีค่านี้ ค่าเริ่มต้นคือ 25 มันนับ superstep ไม่ใช่จำนวนรอบที่วนในลูปของคุณ ดังนั้นมันจึงไม่แมปตรง ๆ กับคำว่า "แก้สามรอบ" และไม่ใช่ตัวแทนของตัวนับที่คุณเขียนเอง เมื่อมันทำงาน มันจะโยน GraphRecursionError และคุณจะเสียผลลัพธ์ที่ทำไปบางส่วน ซึ่งเป็นสิ่งที่คุณไม่อยากให้เกิดตอนจบการรันที่แพง ให้ใช้มันดักบั๊กแบบที่ตัวจัดเส้นทางส่งทุกสถานะกลับไปโหนดเดิม และใช้ MAX_ROUNDS เพื่อบอกว่าคุณอยากให้แก้กี่รอบจริง ๆ
每张图都有这个值,默认 25。它数的是 superstep,不是你在循环里转了几圈,所以它跟「改三次」并不能干净地对上,也不能替代你自己的计数器。它触发时会抛 GraphRecursionError,而你会丢掉已经跑出来的部分结果——这正是你在一次昂贵运行的末尾最不想要的。用它来抓「路由器把所有状态都送回同一个节点」这种 bug;用 MAX_ROUNDS 来表达你真正想要几轮修改。
- Raise
MAX_ROUNDSto 8 and run the rule editor again. Does the writer ever get under five words? - Delete the
roundscheck fromgood_enoughso only the editor can stop it, and see which limit catches you.
- เพิ่ม
MAX_ROUNDSเป็น 8 แล้วรันบรรณาธิการแบบกฎอีกครั้ง นักเขียนลงมาต่ำกว่าห้าคำได้ไหม - ลบการเช็ก
roundsออกจากgood_enoughให้มีแค่บรรณาธิการที่หยุดได้ แล้วดูว่าขีดจำกัดตัวไหนมารับคุณ
- 把
MAX_ROUNDS提到 8,再跑一次规则编辑。写手最后能压到五个词以内吗? - 把
good_enough里的rounds判断删掉,只留编辑能喊停,看看最后是哪一个上限接住了你。
👥 Tutorial 3 — Multi-agent, which is not a new idea · บทที่ 3 — หลายเอเจนต์ ซึ่งไม่ใช่เรื่องใหม่ · 教程 3 —— 多智能体,其实不是新东西
🇬🇧 English
An agent is a node with its own prompt. A supervisor is a node that writes a name into the state. A handoff is a conditional edge reading that name.
You already built all three in Tutorial 2. Nothing new is imported below.
🇹🇭 ไทย
เอเจนต์คือโหนดที่มีพรอมป์ของตัวเอง ผู้ควบคุมคือโหนดที่เขียนชื่อลงในสถานะ การส่งงานต่อคือเส้นเชื่อมมีเงื่อนไขที่อ่านชื่อนั้น
คุณสร้างครบทั้งสามอย่างไปแล้วในบทที่ 2 ข้างล่างนี้ไม่มีการ import อะไรใหม่เลย
🇨🇳 中文
智能体就是一个带自己提示词的节点。主管就是一个把名字写进状态的节点。交接就是一条读那个名字的条件边。
这三样你在教程 2 里已经全建过了。下面没有 import 任何新东西。
5 A supervisor and two specialists · ผู้ควบคุมกับผู้เชี่ยวชาญสองคน · 一个主管,两个专家
📄 supervisor.py
"""Multi-agent, which in LangGraph is not a new concept at all.
An agent is a node with its own prompt. A supervisor is a node that
writes the name of the next one into the state. The handoff everybody
talks about is a conditional edge reading that name.
"""
import os
from operator import add
from typing import Annotated, TypedDict
from langchain_ollama import ChatOllama
from langgraph.graph import START, END, StateGraph
OLLAMA = os.environ.get("OLLAMA_HOST", "http://127.0.0.1:11434")
llm = ChatOllama(model="qwen2.5:14b", base_url=OLLAMA, temperature=0)
MAX_TURNS = 4
SPECIALISTS = {
"transport": "You handle getting around Chiang Mai: airport, songthaews, grab.",
"food": "You handle eating in Chiang Mai: dishes, markets, what to order.",
}
class State(TypedDict):
request: str
# Every specialist appends, so this key needs a reducer or each one
# would wipe out the answer before it.
answers: Annotated[list[str], add]
done: Annotated[list[str], add]
next: str
turns: int
def supervisor(state: State) -> dict:
left = [name for name in SPECIALISTS if name not in state["done"]]
if not left or state["turns"] >= MAX_TURNS:
return {"next": "finish", "turns": state["turns"] + 1}
prompt = (
"A guest wrote: " + state["request"]
+ "\n\nWhich specialist should answer next? Choose one of: "
+ ", ".join(left)
+ ". Reply with the name alone."
)
choice = llm.invoke(prompt).content.strip().lower()
# Same narrowing as the router: the model is choosing, not writing.
picked = left[0]
for name in left:
if name in choice:
picked = name
break
return {"next": picked, "turns": state["turns"] + 1}
def make_specialist(name: str):
def specialist(state: State) -> dict:
prompt = (
SPECIALISTS[name]
+ "\nAnswer only the part of this you are responsible for, "
"in one sentence.\n\n" + state["request"]
)
answer = llm.invoke(prompt).content.strip()
return {"answers": [f"{name}: {answer}"], "done": [name]}
return specialist
def route(state: State) -> str:
return state["next"]
builder = StateGraph(State)
builder.add_node("supervisor", supervisor)
for name in SPECIALISTS:
builder.add_node(name, make_specialist(name))
builder.add_edge(START, "supervisor")
builder.add_conditional_edges(
"supervisor", route,
{"transport": "transport", "food": "food", "finish": END},
)
# Each specialist hands control back, which is what makes it a supervisor
# rather than a fixed pipeline.
for name in SPECIALISTS:
builder.add_edge(name, "supervisor")
graph = builder.compile()
result = graph.invoke({
"request": "We land Friday night and want something local to eat. "
"How do we get in from the airport, and what should we order?",
"answers": [], "done": [], "next": "", "turns": 0,
})
print("supervisor turns:", result["turns"])
print("specialists used:", result["done"])
for answer in result["answers"]:
print(" -", answer[:110])
📋 ONE COMPOUND QUESTION, TWO SPECIALISTS
supervisor turns: 3
specialists used: ['transport', 'food']
- transport: From the airport, take a songthaew or Grab car into Chiang Mai and look for a local restaurant serv
- food: From the airport, take a taxi or Grab car into Chiang Mai city, and once there, try ordering some Khao S
supervisor, and the only thing that travels is the state: the answers list grew, the done list grew, and the supervisor reads done to see who is left. Both of those keys carry Annotated[list, add] for exactly the reason Step 2 showed — two nodes write them, so without the reducer the second specialist would erase the first one's answer and the supervisor would loop forever believing nobody had been.
ไม่มีข้อความอะไรถูกส่งต่อเลย เส้นเชื่อมของผู้เชี่ยวชาญทุกเส้นย้อนกลับไปที่ supervisor และสิ่งเดียวที่เดินทางคือสถานะ ลิสต์ answers ยาวขึ้น ลิสต์ done ยาวขึ้น และผู้ควบคุมอ่าน done เพื่อดูว่าเหลือใคร คีย์ทั้งสองมี Annotated[list, add] ด้วยเหตุผลเดียวกับที่ขั้นที่ 2 แสดงไว้เป๊ะ ๆ คือมีสองโหนดเขียนลงไป ดังนั้นถ้าไม่มี reducer ผู้เชี่ยวชาญคนที่สองจะลบคำตอบของคนแรกทิ้ง และผู้ควบคุมจะวนไม่รู้จบเพราะเชื่อว่ายังไม่มีใครไปเลย
根本没有什么消息在传递。每条专家的边都连回 supervisor,唯一在走的东西是状态:answers 列表变长了,done 列表变长了,而主管读 done 来看还剩谁。这两个键都带着 Annotated[list, add],原因正是第 2 步演示过的那个——有两个节点在写它们,所以没有 reducer 的话,第二个专家会把第一个的答案擦掉,而主管会因为以为没人去过而永远循环下去。
- Remove
Annotated[list[str], add]fromdoneand run it. You have just built the infinite loop thatMAX_TURNSexists to stop. - Add a third specialist for temples. Does the supervisor still pick sensibly when the request never mentions one?
- เอา
Annotated[list[str], add]ออกจากdoneแล้วรัน คุณเพิ่งสร้างลูปไม่รู้จบที่MAX_TURNSมีไว้เพื่อหยุดมัน - เพิ่มผู้เชี่ยวชาญคนที่สามเรื่องวัด ผู้ควบคุมยังเลือกได้สมเหตุสมผลไหมเมื่อคำขอไม่ได้พูดถึงวัดเลย
- 把
done上的Annotated[list[str], add]去掉再跑。你刚刚造出了那个MAX_TURNS专门用来阻止的无限循环。 - 加第三个专家负责寺庙。当请求里压根没提到寺庙时,主管还挑得合理吗?
💾 Tutorial 4 — Memory between runs · บทที่ 4 — ความจำข้ามการรัน · 教程 4 —— 跨次运行的记忆
🇬🇧 English
The feature that is genuinely hard to add yourself, and the one that makes a graph worth the ceremony.
🇹🇭 ไทย
ฟีเจอร์ที่เพิ่มเองยากจริง ๆ และเป็นตัวที่ทำให้กราฟคุ้มกับพิธีรีตองทั้งหมด
🇨🇳 中文
真正很难自己加上去的那个特性,也是让这套繁文缛节值回票价的那个。
6 Checkpoints and thread ids · เช็กพอยต์และ thread id · 检查点与 thread id
📄 memory.py
"""The feature that is hard to bolt on afterwards: the graph can stop,
and start again later knowing what happened.
A checkpointer saves the state after every superstep, filed under a
thread id. Invoke the same graph with the same thread and it carries on.
"""
from operator import add
from typing import Annotated, TypedDict
from langgraph.checkpoint.memory import MemorySaver
from langgraph.graph import START, END, StateGraph
class State(TypedDict):
seen: Annotated[list[str], add]
def remember(state: State) -> dict:
return {"seen": ["visit " + str(len(state["seen"]) + 1)]}
builder = StateGraph(State)
builder.add_node("remember", remember)
builder.add_edge(START, "remember")
builder.add_edge("remember", END)
# Without this argument the graph is stateless between invocations, which
# is the default and the thing people are surprised by.
graph = builder.compile(checkpointer=MemorySaver())
guest = {"configurable": {"thread_id": "guest-1"}}
other = {"configurable": {"thread_id": "guest-2"}}
print("guest-1:", graph.invoke({"seen": []}, guest)["seen"])
print("guest-1:", graph.invoke({"seen": []}, guest)["seen"])
print("guest-1:", graph.invoke({"seen": []}, guest)["seen"])
# A different thread id is a different conversation, same graph object.
print("guest-2:", graph.invoke({"seen": []}, other)["seen"])
# The history is queryable, which is what makes debugging a graph bearable.
history = list(graph.get_state_history(guest))
print("checkpoints kept for guest-1:", len(history))
print("latest state :", graph.get_state(guest).values["seen"])
📋 THREE VISITS, THEN A DIFFERENT GUEST
guest-1: ['visit 1']
guest-1: ['visit 1', 'visit 2']
guest-1: ['visit 1', 'visit 2', 'visit 3']
guest-2: ['visit 1']
checkpoints kept for guest-1: 9
latest state : ['visit 1', 'visit 2', 'visit 3']
get_state_history handing those back is the difference between debugging a graph and guessing at one.
นั่นคือส่วนที่ควรจ้องดู ทุกการเรียกส่งลิสต์ว่างเข้าไป แต่ guest-1 กลับนับขึ้นไปถึงสาม เพราะตัวเช็กพอยต์โหลดสถานะที่บันทึกไว้ก่อน แล้วอินพุตของคุณถูกรวมเข้าไปด้วย reducer แทนที่จะไปแทนที่มัน เปลี่ยน thread id แล้วคุณจะได้กระดานเปล่าจากกราฟที่คอมไพล์แล้วอันเดียวกัน ซึ่งเป็นวิธีที่โปรเซสเดียวให้บริการหลายบทสนทนา เก้าเช็กพอยต์สำหรับการเรียกสามครั้ง เพราะมันบันทึกหลังทุก superstep ไม่ใช่ครั้งเดียวต่อการเรียก และการที่ get_state_history คืนสิ่งเหล่านั้นมา คือความต่างระหว่างการดีบักกราฟกับการเดามัน
这才是值得盯着看的地方。每次调用传进去的都是空列表,而 guest-1 却一路数到三,因为检查点器先把保存的状态加载回来,然后你的输入是被 reducer 合并进去的,而不是替换掉它。换一个 thread id,你就从同一个已编译的图拿到一块白板——这就是一个进程如何服务许多段对话的。三次调用产生九个检查点,因为它是在每个 superstep 之后保存,而不是每次调用存一次;而 get_state_history 能把这些还给你,正是「调试一张图」和「猜一张图」之间的区别。
💸 Tutorial 5 — What it actually costs · บทที่ 5 — ราคาจริงของมัน · 教程 5 —— 它到底要花多少
🇬🇧 English
Same question, same model, same machine. The only variable is the graph, so the difference is the graph's bill.
🇹🇭 ไทย
คำถามเดียวกัน โมเดลเดียวกัน เครื่องเดียวกัน ตัวแปรเดียวคือกราฟ ดังนั้นส่วนต่างคือใบเสร็จของกราฟ
🇨🇳 中文
同一个问题、同一个模型、同一台机器。唯一的变量是那张图,所以差额就是图的账单。
7 Four calls against one, measured · สี่ครั้งเทียบกับหนึ่งครั้ง วัดจริง · 四次调用对一次,实测
📄 measure.py
"""What the supervisor actually costs, against one plain call.
Same question, same model, same machine. The only difference is the
graph, so whatever the numbers say is the graph's bill.
"""
import os
import time
from operator import add
from typing import Annotated, TypedDict
from langchain_ollama import ChatOllama
from langgraph.graph import START, END, StateGraph
OLLAMA = os.environ.get("OLLAMA_HOST", "http://127.0.0.1:11434")
MAX_TURNS = 4
REQUEST = ("We land Friday night and want something local to eat. "
"How do we get in from the airport, and what should we order?")
SPECIALISTS = {
"transport": "You handle getting around Chiang Mai: airport, songthaews, grab.",
"food": "You handle eating in Chiang Mai: dishes, markets, what to order.",
}
class Meter:
"""Wraps the model so every call through the graph is counted."""
def __init__(self):
self.model = ChatOllama(model="qwen2.5:14b", base_url=OLLAMA, temperature=0)
self.calls = 0
self.sent = 0
self.produced = 0
def invoke(self, prompt):
reply = self.model.invoke(prompt)
meta = reply.response_metadata
self.calls += 1
self.sent += meta.get("prompt_eval_count", 0)
self.produced += meta.get("eval_count", 0)
return reply
class State(TypedDict):
request: str
answers: Annotated[list[str], add]
done: Annotated[list[str], add]
next: str
turns: int
def build(meter):
def supervisor(state):
left = [n for n in SPECIALISTS if n not in state["done"]]
if not left or state["turns"] >= MAX_TURNS:
return {"next": "finish", "turns": state["turns"] + 1}
prompt = ("A guest wrote: " + state["request"]
+ "\n\nWhich specialist should answer next? Choose one of: "
+ ", ".join(left) + ". Reply with the name alone.")
choice = meter.invoke(prompt).content.strip().lower()
picked = left[0]
for name in left:
if name in choice:
picked = name
break
return {"next": picked, "turns": state["turns"] + 1}
def make_specialist(name):
def specialist(state):
prompt = (SPECIALISTS[name] + "\nAnswer only the part of this you "
"are responsible for, in one sentence.\n\n" + state["request"])
return {"answers": [meter.invoke(prompt).content.strip()],
"done": [name]}
return specialist
builder = StateGraph(State)
builder.add_node("supervisor", supervisor)
for name in SPECIALISTS:
builder.add_node(name, make_specialist(name))
builder.add_edge(START, "supervisor")
builder.add_conditional_edges(
"supervisor", lambda s: s["next"],
{"transport": "transport", "food": "food", "finish": END})
for name in SPECIALISTS:
builder.add_edge(name, "supervisor")
return builder.compile()
def report(label, meter, seconds, text):
print(f"{label:<12} calls {meter.calls} sent {meter.sent:>4} tok "
f"produced {meter.produced:>4} tok {seconds:5.1f}s")
print(f" {text[:96]}")
single = Meter()
started = time.time()
plain = single.invoke("Answer in two sentences.\n\n" + REQUEST).content.strip()
report("one call", single, time.time() - started, plain.replace("\n", " "))
supervised = Meter()
started = time.time()
result = build(supervised).invoke({"request": REQUEST, "answers": [], "done": [],
"next": "", "turns": 0})
report("supervisor", supervised, time.time() - started, " | ".join(result["answers"]).replace("\n", " "))
print(f"\nthe graph cost {supervised.calls / single.calls:.0f}x the calls and "
f"{(supervised.sent + supervised.produced) / (single.sent + single.produced):.1f}x the tokens")
📋 MEASURED, AND IDENTICAL ACROSS THREE RUNS
one call calls 1 sent 60 tok produced 74 tok 6.0s
To get from the airport to your destination, you can use public transportation like the metro or
supervisor calls 4 sent 328 tok produced 80 tok 4.0s
From the airport, take a songthaew or Grab car into Chiang Mai and look for a local restaurant s
the graph cost 4x the calls and 3.0x the tokens
| One callเรียกครั้งเดียว一次调用 | Supervisorผู้ควบคุม主管版 | |
|---|---|---|
| Model callsจำนวนครั้งที่เรียกโมเดล模型调用次数 | 1 | 4 |
| Tokens sentโทเคนที่ส่งไป发出的 token | 60 | 328 — five and a half times328 ห้าเท่าครึ่ง328 —— 五倍半 |
| Tokens producedโทเคนที่ผลิตออกมา产出的 token | 74 | 80 — barely moved80 แทบไม่ขยับ80 —— 几乎没动 |
| Wall clockเวลาจริง墙上时间 | 6.1 s | 3.9 s — faster, despite four calls3.9 วิ เร็วกว่า ทั้งที่เรียกสี่ครั้ง3.9 秒 —— 更快,尽管调了四次 |
| Answer qualityคุณภาพคำตอบ答案质量 | invented a metroกุรถไฟใต้ดินขึ้นมา编出了一条地铁 | songthaew, Grab, khao soiสองแถว Grab ข้าวซอย双条车、Grab、khao soi |
measure.py against your own.
เพราะการสร้างข้อความคือส่วนที่กินเวลาหลักบนโมเดลในเครื่อง และการเรียกสี่ครั้งผลิตรวมกัน 80 โทเคน เทียบกับ 74 โทเคนในครั้งเดียว ขณะที่โทเคนอินพุตส่วนเกินถูกประมวลผลแบบขนานและแทบไม่มีต้นทุน บน API ที่โฮสต์ไว้และคิดเงินต่อโทเคน ลำดับนี้จะกลับด้าน คุณจะจ่ายสามเท่าและรอนานกว่า เพราะแต่ละครั้งคือการวิ่งไปกลับผ่านเครือข่ายแยกกัน อย่าเอาตัวเลขนี้ไปใช้กับสภาพแวดล้อมอื่น ให้รัน measure.py กับของคุณเอง
因为在本地模型上生成才是大头,而四次调用一共产出 80 个 token,对比一次产出 74 个;与此同时多出来的输入 token 是并行处理的,几乎不花时间。在按 token 计费的托管 API 上,这个顺序会反过来:你会付三倍的钱,还等得更久,因为每次调用都是一次独立的网络往返。别把这个数字搬到别的环境——拿 measure.py 去跑你自己的。
🪤 Where this falls over · มันพังตรงไหน · 它在哪里翻车
| Problemปัญหา问题 | What actually happensเกิดอะไรขึ้นจริง实际会怎样 |
|---|---|
| The missing reducerreducer ที่หายไป缺失的 reducer | Step 2, and it never stops being the answer. Silent data loss with no warning, and in a supervisor graph it also causes an infinite loop, because the list tracking who has finished keeps getting wiped.ขั้นที่ 2 และมันเป็นคำตอบเสมอไม่มีวันหมด ข้อมูลหายเงียบ ๆ โดยไม่มีคำเตือน และในกราฟผู้ควบคุมมันยังทำให้เกิดลูปไม่รู้จบด้วย เพราะลิสต์ที่ติดตามว่าใครทำเสร็จแล้วถูกล้างซ้ำ ๆ第 2 步,而且它永远都还是那个答案。无声的数据丢失,没有任何警告;在主管图里它还会造成无限循环,因为记录谁已完成的那个列表一直被清掉。 |
| Trusting the model to stopเชื่อว่าโมเดลจะหยุดเอง指望模型自己停 | Step 4 measured it: told a countable rule, the model approved a draft that broke it. Any loop whose only exit is the model's agreement needs a counter you wrote.ขั้นที่ 4 วัดไว้แล้ว เมื่อได้รับกฎที่นับได้ โมเดลอนุมัติร่างที่ผิดกฎนั้น ลูปไหนก็ตามที่มีทางออกทางเดียวคือโมเดลเห็นด้วย ต้องมีตัวนับที่คุณเขียนเอง第 4 步已经量过了:给了一条可数的规则,模型批准了一份违反它的草稿。任何唯一出口是「模型同意」的循环,都需要一个你自己写的计数器。 |
| Specialists overlappingผู้เชี่ยวชาญทำงานทับกัน专家互相越界 | Step 5. Told to answer only their own part, both answered the transport question. You pay twice and eventually get two answers that disagree.ขั้นที่ 5 ทั้งที่ถูกสั่งให้ตอบเฉพาะส่วนของตัวเอง ทั้งคู่กลับตอบคำถามเรื่องการเดินทาง คุณจ่ายสองครั้งและสุดท้ายจะได้คำตอบสองอันที่ขัดกัน第 5 步。明明被要求只答自己那部分,两个都回答了交通问题。你付两次钱,最终还会拿到两个互相矛盾的答案。 |
| Cost grows with agentsค่าใช้จ่ายโตตามจำนวนเอเจนต์成本随智能体数量增长 | Step 7. Sent tokens rose five and a half times while produced tokens stayed flat, because the request is re-sent to everyone. Adding a fourth specialist adds its share again.ขั้นที่ 7 โทเคนที่ส่งไปเพิ่มขึ้นห้าเท่าครึ่งขณะที่โทเคนที่ผลิตแทบไม่ขยับ เพราะคำขอถูกส่งซ้ำให้ทุกคน การเพิ่มผู้เชี่ยวชาญคนที่สี่ก็เพิ่มส่วนของมันเข้าไปอีก第 7 步。发出的 token 涨了五倍半,而产出的 token 基本没动,因为请求被重发给了每个人。再加第四个专家,就再加一份。 |
| MemorySaver in productionMemorySaver บนโปรดักชัน拿 MemorySaver 上生产 | It is a dictionary. Every tutorial uses it, and it loses every conversation on restart. Swap the checkpointer before you deploy, not after.มันคือดิกชันนารี ทุกบทเรียนใช้มัน และมันทำบทสนทนาทั้งหมดหายตอนรีสตาร์ต ให้เปลี่ยนตัวเช็กพอยต์ก่อนดีพลอย ไม่ใช่หลังจากนั้น它就是个字典。每份教程都在用它,而它一重启就丢掉所有对话。在上线之前换掉检查点器,不是之后。 |
| Is a graph even needed?จำเป็นต้องใช้กราฟจริงไหม真的需要一张图吗 | If your flow is A then B with an if in the middle, a function does it with less ceremony. The graph earns its place when you need to resume, branch visibly, or let more than one writer touch the same state.ถ้าโฟลว์ของคุณคือ A แล้ว B โดยมี if ตรงกลาง ฟังก์ชันธรรมดาก็ทำได้โดยไม่ต้องมีพิธีรีตอง กราฟจะคุ้มก็ต่อเมื่อคุณต้องทำงานต่อจากจุดเดิม แตกกิ่งแบบมองเห็นได้ หรือให้มีผู้เขียนมากกว่าหนึ่งรายแตะสถานะเดียวกัน如果你的流程就是 A 然后 B、中间一个 if,用一个函数就行,省掉这套仪式。图值回票价的时候,是你需要断点续跑、需要看得见的分支,或者需要不止一个写入者去碰同一份状态。 |
🇬🇧 English
Next: smolagents, which gives the model a Python interpreter instead of a graph, CrewAI and AutoGen, or Jev — where the routing decision comes back as a probability instead of a word you have to narrow.
🇹🇭 ไทย
ต่อไป: smolagents ซึ่งให้ตัวแปล Python กับโมเดลแทนที่จะให้กราฟ CrewAI และ AutoGen หรือ Jev ที่การตัดสินใจเรื่องเส้นทางกลับมาเป็นความน่าจะเป็น แทนที่จะเป็นคำที่คุณต้องมาทำให้แคบลง
🇨🇳 中文
接下来:smolagents,它给模型的是一个 Python 解释器而不是一张图;CrewAI 和 AutoGen;或者 Jev——那里路由决策回来的是一个概率,而不是一个还要你去收窄的词。