langgraph-api源码分析2-一次对话
1.请求数据
处理对话入口为:
http://{ip:port}/threads/{thread_id/runs/stream
其中的thread_id是在创建线程中返回给前端的线程id
请求报文如下:
{
"input": {
"messages": [
{
"id": "8c0a8d55-d590-43f9-8c16-de782fa5ccbf",#一次请求唯一标识
"type": "human",
"content": [
{
"type": "text",
"text": "请客观评价一下张邦昌"
}
]
}
]
},
"stream_mode": [ #参见流模式
"values",
"messages-tuple",
"custom"
],
"stream_subgraphs": true,
"stream_resumable": true,
"assistant_id": "agent", #使用的agent唯一标识
"on_disconnect": "continue"
}
2.数据模型
涉及数据库表包括assistant,run和thread表,thread,分别存放助手数据、run数据和线程数据,其中thread和run表是1:n的关系。
assistant定义如下:

run定义如下,其中kwargs存放请求数据内容:

3.源码分析
对应入口函数为langgraph-api/api/runs.py文件中的stream_run方法,具体源码如下:
"""
分析。langgraph-api中使用了redis的PUB/SUB机制,完成请求方和处理方解耦,同时保证了处理的实时性。
该方法本身很简洁,调用Runs.Stream.subscribe方法提交请求,订阅
thread:{thread_id}:runn:{run_id}:stream频道,并通过在redis的
LIST_RUN_QUEUE队列中写入数据来唤醒后台任务。
然后调用Runs.Stream.join从订阅频道读取后台任务发布的数据,并以流式方式应答。
所以重点分析这两个方法。
"""
async def stream_run(
request: ApiRequest,
):
""创建一个run,对应一次请求"""
thread_id = request.path_params["thread_id"]
payload = await request.json(RunCreateStateful)
on_disconnect = payload.get("on_disconnect", "continue")
run_id = uuid7() #后端生成run_id
async with await Runs.Stream.subscribe(run_id, thread_id) as sub:#订阅处理器
async with connect() as conn: #conn是到postgreas的数据库连接
run = await create_valid_run(#创建一个run并保存在数据库中
conn,
thread_id,
payload,
request.headers,
run_id=run_id,
request_start_time=request.scope.get("request_start_time_ms"),
)return EventSourceResponse(
Runs.Stream.join(
run["run_id"],
thread_id=thread_id,
cancel_on_disconnect=on_disconnect == "cancel",
stream_channel=sub,
last_event_id=None,
),
headers={
"Location": f"/threads/{thread_id}/runs/{run['run_id']}/stream",
"Content-Location": f"/threads/{thread_id}/runs/{run['run_id']}",
},
)
Runs.Stream在langgraph_runtime_postgres目录ops.py中定义,其subscribe方法源码如下:
@staticmethod
async def subscribe(
run_id: UUID,
thread_id: UUID | None = None,
) -> StreamHandler:
"""Subscribe to the run stream, returning a stream handler.
The stream handler must be passed to `join` to receive messages.""""""
channel格式如下:
thread:2f0a1fd1-5281-4769-b7f0-c0fcb7382622:
run:01997549-519e-721f-8642-e83573178d63:stream
channel_old格式如下:
run:01997549-519e-721f-8642-e83573178d63:stream
pattern格式如下:
channel:*
pattern_old格式如下:
channel_old:*
"""
channel = CHANNEL_RUN_STREAM.format(thread_id, run_id)
channel_old = CHANNEL_RUN_STREAM_OLD.format(run_id)
# Keeping the pattern for rollout so that existing other streams still get picked up
pattern = CHANNEL_RUN_STREAM.format(thread_id, run_id) + ":*"
pattern_old = CHANNEL_RUN_STREAM_OLD.format(run_id) + ":*"
pubsub = await get_pubsub(#订阅channel,channel_old和通配模式频道
channels=(
channel,
channel_old,
),
patterns=(
pattern,
pattern_old,
),
)
return pubsub#返回流处理器句柄
在langgraph-api/models/run.py文件中的create_valid_run方法做一下分析,代码如下:
async def create_valid_run(
conn: AsyncConnectionProto,
thread_id: str | None,
payload: RunCreateDict,
headers: Mapping[str, str],
barrier: asyncio.Barrier | None = None,
run_id: UUID | None = None,
request_start_time: float | None = None,
temporary: bool = False,#未传递该参数,所以为False
) -> Run:
request_id = headers.get("x-request-id") # 当前为空
(
assistant_id, #agent唯一标识
thread_id_, #前端带入
checkpoint_id, #从payload中提取
run_id, #前面生成的
) = _get_ids(
thread_id,
payload,
run_id=run_id,
)
if (#暂不考虑分支
(thread_id_ is None or temporary)
and (command := payload.get("command"))
and command.get("resume")
):
raise HTTPException(
status_code=400,
detail="You must provide a thread_id when resuming.",
)
temporary = (temporary or thread_id_ is None) and payload.get(
"on_completion", "delete"
) == "delete" #暂不考虑
stream_resumable = payload.get("stream_resumable", False)#暂不考虑真假#stream_mode=["values","messages-tuple","custom"], multitask_strategy=‘queue’
#prevent_insert_if_inflight=False
stream_mode, multitask_strategy, prevent_insert_if_inflight = assign_defaults(
payload
)
# 配置和上下文,均不需要考虑
config = payload.get("config") or {}
context = payload.get("context") or {}"""
configurable初始化为{"configurable":{}},
然后增加chepoint_id后为{"configurable":{}, "checkpoint_id":id},
然后增加"checkpoint_ns":""
"""
configurable = config.setdefault("configurable", {})if configurable and context:
raise HTTPException(
status_code=400,
detail="Cannot specify both configurable and context. Prefer setting context alone. Context was introduced in LangGraph 0.6.0 and is the long term planned replacement for configurable.",
)# 如果payload中有context,则拷贝给configurable
if context:
configurable = context.copy()
config["configurable"] = configurable
else: #context为{‘configurable':{}}
context = configurable.copy()if checkpoint_id:#如果payload中有checkpoint_id,则写入configurable
configurable["checkpoint_id"] = str(checkpoint_id)#如果payload中有checkpoint ,则写入configurable
if checkpoint := payload.get("checkpoint"):
configurable.update(checkpoint)#把http header中的信息增加到configurable中
configurable.update(get_configurable_headers(headers))ctx = get_auth_ctx()#暂不考虑身份认证,此时user_id为None
if ctx:
user = cast(BaseUser | None, ctx.user)
user_id = get_user_id(user)
configurable["langgraph_auth_user"] = user
configurable["langgraph_auth_user_id"] = user_id
configurable["langgraph_auth_permissions"] = ctx.permissions
else:
user_id = None
if not configurable.get("langgraph_request_id"):
configurable["langgraph_request_id"] = request_id
if ls_tracing := payload.get("langsmith_tracer"):
configurable["__langsmith_project__"] = ls_tracing.get("project_name")
configurable["__langsmith_example_id__"] = ls_tracing.get("example_id")
if request_start_time:
configurable["__request_start_time_ms__"] = request_start_time
after_seconds = cast(int, payload.get("after_seconds", 0))
configurable["__after_seconds__"] = after_seconds
put_time_start = time.time()
if_not_exists = payload.get("if_not_exists", "reject")#当前仅考虑为rejectdurability = payload.get("durability")
if durability is None:
checkpoint_during = payload.get("checkpoint_during")
durability = "async" if checkpoint_during in (None, True) else "exit"#直接看这里。Runs.put插入数据到run表,并完成thread表的更新
run_coro = Runs.put(
conn,
assistant_id,
{#这里的数据会保存到run表的kwargs字段中
"input": payload.get("input"),
"command": payload.get("command"),
"config": config,
"context": context,
"stream_mode": stream_mode,
"interrupt_before": payload.get("interrupt_before"),
"interrupt_after": payload.get("interrupt_after"),
"webhook": payload.get("webhook"),
"feedback_keys": payload.get("feedback_keys"),
"temporary": temporary,
"subgraphs": payload.get("stream_subgraphs", False),
"resumable": stream_resumable,
"checkpoint_during": payload.get("checkpoint_during", True),
"durability": durability,
},
metadata=payload.get("metadata"),
status="pending",
user_id=user_id,
thread_id=thread_id_,
run_id=run_id,
multitask_strategy=multitask_strategy,
prevent_insert_if_inflight=prevent_insert_if_inflight,
after_seconds=after_seconds,
if_not_exists=if_not_exists,
)
run_ = await run_coro #在Runs.put中插入的run信息迭代器if barrier:#暂不考虑
await barrier.wait()# abort if thread, assistant, etc not found
try:
first = await anext(run_)
except StopAsyncIteration:
raise HTTPException(
status_code=404, detail="Thread or assistant not found."
) from None# handle multitask strategy
inflight_runs = [run async for run in run_]
if first["run_id"] == run_id:
logger.info(
"Created run",
run_id=str(run_id),
thread_id=str(thread_id_),
assistant_id=str(assistant_id),
multitask_strategy=multitask_strategy,
stream_mode=stream_mode,
temporary=temporary,
after_seconds=after_seconds,
if_not_exists=if_not_exists,
stream_resumable=stream_resumable,
run_create_ms=(
int(time.time() * 1_000) - request_start_time
if request_start_time
else None
),
run_put_ms=int((time.time() - put_time_start) * 1_000),
checkpoint_id=str(checkpoint_id),
)
# 以下if分支后继再分析
if multitask_strategy in ("interrupt", "rollback") and inflight_runs:
with contextlib.suppress(HTTPException):
# if we can't find the inflight runs again, we can proceeed
await Runs.cancel(
conn,
[run["run_id"] for run in inflight_runs],
thread_id=thread_id_,
action=multitask_strategy,
)
return first #直接返回run数据
elif multitask_strategy == "reject":
raise HTTPException(
status_code=409,
detail="Thread is already running a task. Wait for it to finish or choose a different multitask strategy.",
)
else:
raise NotImplementedError
Runs.put方法在langgraph_runtime_postgres目录下的ops.py文件中,源码如下:
本方法主要逻辑如下:
1)查询线程数据保存到临时表run_thread
2)计算run_thread和assistant的笛卡尔积,并筛选当前会话thread_id和assistant_id相同的记录
3)根据以上的查询结果和传入的参数组织数据插入到run表中,状态为pending,结果保存在inserted_run中
4)对insert_run和assistant做inner join,并与传入的参数一起作为物料更新thread表的配置数据和元数据,并修改线程状态为busy
5)执行完以上处理后,在reids的LIST_RUN_QUEUE队列中插入[1],从而唤醒worker
@staticmethod
async def put(
conn: AsyncConnection[DictRow],
assistant_id: UUID,
kwargs: dict,
*,
thread_id: UUID | None = None,
user_id: str | None = None,
run_id: UUID | None = None,
status: RunStatus | None = "pending",
metadata: MetadataInput,
prevent_insert_if_inflight: bool,
multitask_strategy: MultitaskStrategy = "reject",
if_not_exists: IfNotExists = "reject", #if_not_exists="reject"
after_seconds: int = 0,#传入参数为0,所以立刻唤醒worker
ctx: Auth.types.BaseAuthContext | None = None,
) -> AsyncIterator[Run]:
……#上面的非核心代码暂不关注
if FF_RICH_THREADS:"""
下面的SQL把参数中的thread_id、"busy"和assistant表中的字段拼接成
的metadata和config插入到thread表中,如果表中已经有线程记录,则执行更新操作
"""
insert_thread_sql = """
INSERT INTO thread (thread_id, status, metadata, config)
SELECT
%(thread_id)s,
'busy',
jsonb_build_object(
'graph_id', assistant.graph_id,
'assistant_id', assistant.assistant_id
) || coalesce(%(config)s::jsonb -> 'metadata', '{}') || %(metadata)s::jsonb,
assistant.config
|| %(config)s::jsonb
|| jsonb_build_object(
'configurable',
coalesce((assistant.config -> 'configurable'), '{}')
)
FROM assistant
WHERE assistant_id = %(assistant_id)s
ON CONFLICT (thread_id) DO UPDATE
-- Return existing thread; otherwise the CTE below could return nothing
-- if there's a conflict here in an uncommitted transaction in another worker
-- but it hasn't been inserted to be available for the SELECT statement below
SET status = 'busy'
RETURNING thread.*
"""
else:
……//暂不关注
thread_query_cte = (
f"""WITH run_thread AS ({insert_thread_sql}), """
if thread_id is None
else (
f"""WITH inserted_thread AS (
{insert_thread_sql}
),
run_thread AS (
SELECT * FROM thread where thread_id = %(thread_id)s {filter_clause}
UNION
SELECT * FROM inserted_thread
LIMIT 1
),
{ttl_insert_query}"""
if if_not_exists == "create""""
正常情况走这里else分支。
查询线程表,并保存到临时表run_thread表中,供inserted_run调用
thread_query_cte = 'WITH run_thread AS (
SELECT * FROM thread WHERE thread_id = %(thread_id)s
{filter_clause}), {ttl_insert_query}'"""
else f"""WITH run_thread AS (
SELECT * FROM thread
WHERE thread_id = %(thread_id)s
{filter_clause}),
{ttl_insert_query}"""
)
)params = {
"multitask_strategy": multitask_strategy,
"run_id": run_id or uuid7(),
"thread_id": thread_id or uuid4(),
"assistant_id": assistant_id,
"metadata": Jsonb(metadata),
"kwargs": Jsonb(kwargs),
"config": Jsonb(kwargs.get("config")),
"status": status,
"user_id": user_id,
"after_seconds": f"{after_seconds} second",
"strategy": strategy,
"env_strategy": env_strategy,
"ttl_interval": ttl_interval_minutes,
"env_ttl_interval": env_ttl_interval_minutes,
}
params.update(filter_params)#needs_inflight为False
needs_inflight = thread_id is not None and (
multitask_strategy in ("rollback", "interrupt")
or prevent_insert_if_inflight
)
query = thread_query_cteif needs_inflight:
query += f"""
inflight_runs AS (
SELECT run.*
FROM run
{thread_join}
WHERE thread_id = %(thread_id)s AND run.status in ('pending', 'running') {filter_clause}
),
"""query += (
"""
inserted_run AS (#insert_run供updated_thread调用。数据插入run表
INSERT INTO run (run_id, thread_id, assistant_id, metadata, status, kwargs, multitask_strategy, created_at)
SELECT #从函数参数及后面的笛卡尔积中选择插入到run表的字段
%(run_id)s,
thread_id,
assistant_id,
%(metadata)s,
%(status)s,
%(kwargs)s::jsonb || jsonb_build_object(#记住kwargs包括了对话请求数据
'config', assistant.config || run_thread.config || %(config)s::jsonb || jsonb_build_object(
'configurable',
coalesce((assistant.config -> 'configurable'), '{}') ||
coalesce((run_thread.config -> 'configurable'), '{}') ||
coalesce(%(config)s::jsonb -> 'configurable', '{}') ||
jsonb_build_object(
'run_id', %(run_id)s::text,
'thread_id', thread_id,
'graph_id', graph_id,
'assistant_id', assistant_id,
'user_id', coalesce(
%(config)s::jsonb -> 'configurable' ->> 'user_id',
run_thread.config -> 'configurable' ->> 'user_id',
assistant.config -> 'configurable' ->> 'user_id',
%(user_id)s::text
)
),
'metadata',
assistant.metadata || run_thread.metadata || coalesce(%(config)s::jsonb -> 'metadata', '{}') || %(metadata)s
),
'context', coalesce(assistant.context, '{}') || coalesce(%(kwargs)s::jsonb -> 'context', '{}')
),
%(multitask_strategy)s,
now() + %(after_seconds)s::interval"""
run_thread临时表保存的是当前线程信息,assistant保存的是助手信息。
计算两个表的笛卡尔积,并从中选择与参数中的thread_id和assistant_id相同的记录,
这个结果作为SELECT的源数据
"""
FROM run_thread
CROSS JOIN assistant
WHERE thread_id = %(thread_id)s
AND assistant_id = %(assistant_id)s"""
+ (
" AND NOT EXISTS (SELECT 1 FROM inflight_runs)"
if prevent_insert_if_inflight and thread_id is not None
else ""
)
+ """ RETURNING run.*
)"""
)
if FF_RICH_THREADS:
query += """,
updated_thread AS (#以上面插入的run数据作为物料更新线程元数据和配置数据
UPDATE thread SET
metadata = jsonb_set(
jsonb_set(thread.metadata, '{graph_id}', to_jsonb(assistant.graph_id)),
'{assistant_id}',
to_jsonb(assistant.assistant_id)
),
config = assistant.config
|| thread.config
|| %(config)s::jsonb
|| jsonb_build_object(
'configurable',
coalesce((assistant.config -> 'configurable'), '{}') ||
coalesce(thread.config -> 'configurable', '{}')
),
status = 'busy'"""
以下SQL内部连接刚刚插入的run记录和assistant,并且当前线程状态不是'busy'
"""
FROM inserted_run
INNER JOIN assistant
ON assistant.assistant_id = inserted_run.assistant_id
WHERE
thread.thread_id = inserted_run.thread_id AND thread.status != 'busy'
)"""
if needs_inflight:
query += """
SELECT * FROM inserted_run
UNION ALL
SELECT * FROM inflight_runs
"""
else:
query += """#返回对应的run记录
SELECT * FROM inserted_run
"""
cur = await conn.execute(query, params, binary=True)#异步执行以上的SQL操作async def consume() -> AsyncIterator[Run]:#处理执行批量SQL后返回的结果
async for row in cur:
yield row
if row["run_id"] == run_id: #确保是当前run的数据
# inserted run, notify queue
if not after_seconds: #当前走本分支,马上唤醒worker
await wake_up_worker() #实际是在redis的LIST_RUN_QUEUE队列中插入[1]
else:
create_task(wake_up_worker(after_seconds))return consume()
前面已经完成stream_run方法前半部分的分析,下面分析Runs.Stream.join方法,源码如下:
#直接看最核心的代码,把流程贯穿起来。
#直接从订阅频道获取数据,并以流式返回。
@staticmethod
async def join(
run_id: UUID,
*,
stream_channel: StreamHandler,
thread_id: UUID,
ignore_404: bool = False,
cancel_on_disconnect: bool = False,
stream_mode: StreamMode | list[StreamMode] | None = None,
last_event_id: str | None = None,
ctx: Auth.types.BaseAuthContext | None = None,
) -> AsyncIterator[tuple[bytes, bytes, bytes | None]]:"""Stream the run output, either from a stream handler or a stream mode."""
start_time = datetime.now(UTC)
await Runs.Stream.check_run_stream_auth(run_id, thread_id, ctx)pubsub: StreamHandler = stream_channel
try:
logger.info(
"Joined run stream",
run_id=str(run_id),
thread_id=str(thread_id),
cancel_on_disconnect=cancel_on_disconnect,
)#状态检查。如果流已经结束,则清空流,否则设置下一次超时时间为0.1秒
run_status_string = RUN_STATUS_STRING.format(thread_id, run_id)
if value := await get_redis().get(run_status_string):
if value == b"done":
# if already done, we can drain the stream
timeout: int | float = DRAIN_TIMEOUT
else:
timeout = WAIT_LESS_TIMEOUT
else:#如果为从redis获取到流状态,则设置下一次超时时间为0.1秒
# Start with shorter timeout for cases after the control channel has expired or the run doesn't exist
timeout = WAIT_LESS_TIMEOUT
highest_stream_id: str | None = None……#中间代码省略
# stream events
while True:#直接从订阅的频道获取数据,保存到store中
if store := await pubsub.get_message(timeout=timeout):
if LOG_LEVEL_DEBUG:
await logger.adebug(
"Received redis stream event",
run_id=str(run_id),
type=store["type"],
channel=store["channel"],
data=store["data"],
)
if store["type"].encode() in pubsub.SUBUNSUB_MESSAGE_TYPES:
# This is a subscription message, not a data message
pass
else:
decoded = decode_stream_message(
store["data"], channel=store["channel"]
)
event = decoded.event_bytes
event_name = event.decode("utf-8")
message = decoded.message_bytes
len_str = decoded.stream_id_bytes
if event_name == "control":
if message == b"done":
timeout = DRAIN_TIMEOUT
else:
timeout = WAIT_LESS_TIMEOUT#因为未传入stream_mode,所以走这个分支
elif (
not stream_mode
or event_name in stream_mode
or (
(
"messages" in stream_mode
or "messages-tuple" in stream_mode
)
and event_name.startswith("messages")
)
):
yield (#这里就是返回前端的流式数据
event,
message,
len_str,
)
elif timeout == DRAIN_TIMEOUT:#如果流已传输完成,则跳出循环
break
else:#重新从订阅频道中获取数据
async with connect() as conn:
run_iter = await Runs.get(
conn, run_id, thread_id=thread_id, ctx=ctx
)
run = await anext(run_iter, None)#如果未获取到run数据或者状态不是'pending'或'running',则设置超时为
#DRAIN_TIMEOUT
if run is None or run["status"] not in (
"pending",
"running",
):
timeout = DRAIN_TIMEOUT#如果后台任务正在运行,但从订阅频道中未获取到事件信息,
#则设置超时时间为5秒
else:
timeout = WAIT_TIMEOUTif run is None and not ignore_404:
yield (
b"error",
json_dumpb(
HTTPException(
status_code=404,
detail=f"Run with ID '{run_id}' not found. Please verify the ID is correct and the run hasn't been deleted or expired.",
)
),
None,
)
except asyncio.CancelledError:
if cancel_on_disconnect:
create_task(cancel_run(thread_id, run_id))# Don't do anything before cancelling the run to minimize race conditions
elapsed_time = (datetime.now(UTC) - start_time).total_seconds()
await logger.awarning(
f"Client disconnected after {elapsed_time} seconds. Consider adjusting the client or network timeouts if this is unexpected.",
run_id=str(run_id),
elapsed_time=elapsed_time,
)
raise
3.处理流程
在一次对话处理中主要流程如下图所示:
到这里已经完成了一次对话请求和应答的分析,先订阅该线程本次会话的频道,然后从频道中获取发布的数据,并以流式返回。但agent的应答数据是如何发布到对应频道的呢?这个问题在下一篇博文中讲解。
更多推荐
所有评论(0)