如何调整 langchain 中 structured chat agent 的输出格式?
固定格式应由应用代码或回调处理,Observation 直接使用工具返回值。只改 prompt,无法保证 verbose=True 的控制台日志按指定格式显示;把 stop_sequence 设为 False 也解决不了这个问题。
所有标签都可以按固定格式显示,但模型没有输出的 Thought 无法补回。这时应标注“未提供”,避免编造推理内容。
为什么缺少 Observation 标签
模型输出、工具结果和控制台日志分别由不同部分控制:
| 内容 | 来源 | 如何控制 |
|---|---|---|
| 模型生成的 action、回答文本 | 模型 | prompt 和 output parser |
| SQL 查询结果 | 实际执行的工具 | 工具实现和数据库查询 |
| 控制台标签、换行、颜色 | 日志回调 | callback handler |
默认 StdOutCallbackHandler 会打印工具返回值,只有收到 observation_prefix 时才会打印对应前缀。因此,原始数据直接接在 action 后面,符合默认回调的行为。缺少标签本身不能证明查询没有执行。源码:StdOutCallbackHandler
传回模型的历史记录与控制台日志也有区别。create_structured_chat_agent 使用的 format_log_to_str 会将已执行的 action 和 observation 整理进 agent_scratchpad,其中包含 Observation: 和 Thought: 前缀。源码:format_log_to_str
Question 通常已作为输入传入,不一定会再次出现在模型输出中。Thought 也不是每次 action 都有的独立字段,不能仅凭日志中没有它,就认定内容被截断。
保留 agent 所需的 JSON 协议
Structured chat agent 使用包含 action、action_input 的 JSON 表达工具调用,例如:
{
"action": "sql_db_query",
"action_input": {
"query": "SELECT COUNT(*) FROM orders"
}
}
最终回答也使用这套协议:
{
"action": "Final Answer",
"action_input": "最终回答内容"
}
sql_db_query 必须替换为实际注册的工具名称。不要为了调整控制台排版,把 agent 的输出协议直接改成纯文本 Action Input:、Final Answer:,否则可能导致解析失败。展示时再把 JSON 字段转换成所需标签即可。源码:JSONAgentOutputParser
执行完成后,统一排版输出
要保存格式稳定的执行记录,可以启用 return_intermediate_steps=True。返回结果中的每一步都包含 AgentAction 和对应的 observation,可直接用于展示。源码:AgentExecutor
将以下代码接在已有的 llm、tools、prompt 初始化代码之后。示例使用 langchain_classic 导入路径;如果项目仍使用提供这些接口的旧版 langchain,请按实际安装版本调整导入路径。
import json
import re
from langchain_classic.agents import (
AgentExecutor,
create_structured_chat_agent,
)
agent = create_structured_chat_agent(
llm=llm,
tools=tools,
prompt=prompt,
stop_sequence=True,
)
executor = AgentExecutor(
agent=agent,
tools=tools,
verbose=False,
return_intermediate_steps=True,
)
def extract_visible_thought(log):
# 只提取模型确实输出的 Thought 文本。
match = re.search(
r"(?:^|\n)Thought:\s*(.*?)(?=\nAction:|\Z)",
log,
flags=re.DOTALL,
)
if match and match.group(1).strip():
return match.group(1).strip()
return "(未提供可见的 Thought 文本)"
question = "orders 表共有多少条记录?"
result = executor.invoke({"input": question})
print(f"Question: {question}")
for action, observation in result["intermediate_steps"]:
print(f"Thought: {extract_visible_thought(action.log)}")
print(f"Action: {action.tool}")
print("Action Input:")
print(json.dumps(
action.tool_input,
ensure_ascii=False,
indent=2,
default=str,
))
print("Observation:")
print(observation)
print("Thought: (最终阶段的 Thought 未单独返回)")
print("Final Answer:")
print(result["output"])
代码直接打印每一步保存的工具返回值,无需让模型重新生成 Observation。执行查询工具时,展示的是该次调用返回的内容;执行 schema 查询或 SQL 检查工具时,展示的是相应工具的结果;发生错误时,则展示错误信息。
extract_visible_thought 只按 Thought: 文本约定提取内容,无法适配所有输出形式。模型只返回 JSON 时,显示“未提供”是正常的。intermediate_steps 也不单独包含最终回答阶段的 Thought,因此代码为这一阶段保留了占位说明。
需要实时查看时,自定义回调
要在实时日志中补上 Observation:,可以继承默认 stdout handler,修改工具执行结束时的打印方式:
from langchain_core.callbacks import StdOutCallbackHandler
class ObservationPrinter(StdOutCallbackHandler):
def on_tool_end(self, output, **kwargs):
print("\nObservation:", flush=True)
print(output, flush=True)
# executor 使用上面创建的实例,verbose=False。
# 通过调用时的 config 传入,回调可传递给内部工具。
result = executor.invoke(
{"input": question},
config={"callbacks": [ObservationPrinter()]},
)
这个回调会补齐 observation 标签,action 和结束日志仍按默认 handler 的方式打印。如果需要实时统一全部标签,可以继续实现 on_agent_action 和 on_agent_finish,分别读取 action 的工具字段和 finish 的返回值。
使用自定义回调时,应关闭相关对象的默认 verbose 输出,避免两套日志重复打印。
stop_sequence 和数据完整性需要分开检查
模型支持 stop sequence 时,可以保留 stop_sequence=True。它会在模型生成到 observation 边界时停止生成,由执行器执行工具,再将真实结果传给后续模型调用。它不会自动给控制台添加标签,也不能保证最终回答完全正确。具体停止字符串以所安装版本的实现为准。源码:create_structured_chat_agent
排查时还需注意:
- 示例日志调用的是
tavily_search_results_json,返回的是搜索结果。排查 SQL 场景时,应核对实际的action.tool和action.tool_input。 - 工具返回值可能已受到 SQL 的
LIMIT、工具实现或结果字符串长度设置的限制。重新排版无法恢复上游省略的数据。 - 如果
action.log本身不完整,再检查模型响应长度限制、停止条件和终端显示。仅凭现有片段,无法确定是哪一层发生了截断。 Final Answer仍是 agent 返回的内容。核对具体数据时,应以查询工具返回的 observation 为依据。
备注:内容仅供参考。
