127.Agent-LangChain核心组件-模型输出后json修复

内容参考于:图灵AI大模型全栈

摘要 :本文通过一个完整的 LangChain Agent 中间件实战案例,演示如何利用 before_agent 和 after_model 装饰器实现用户消息预处理与模型输出 JSON 格式的自动修复。文章详细讲解了 repair_json_string 函数中三个正则表达式的修复逻辑(去除 Markdown 代码块标记、删除多余逗号、为未加引号的 key 补引号),并展示了如何将修复后的 JSON 写回消息状态、为 Agent 注册模型输出后的处理函数,最终实现一个可自动纠错 JSON 的智能体。

效果图:

下图红框是输入的错误json,下图蓝框是验证json,下图绿框是修复json

修复逻辑如下图红框的三个正则表达式的处理,这里只是简单的处理,如果是复杂的可以通过大模型来搞

下图红框是把新json写到状态里,下图绿框是给Agent添加模型输出后要执行的函数

代码

python 复制代码
from dataclasses import dataclass
from langchain.agents import create_agent, AgentState
from langchain_core.messages import HumanMessage, AIMessage, SystemMessage
import re
import json
from langgraph.runtime import Runtime
from typing import Callable
from langchain_qwq import ChatQwen
from dotenv import load_dotenv
import os

# 加载模型
# 加载环境变量
load_dotenv()

# 初始化模型
llm = ChatQwen(
    model="qwen3.7-flash",
    api_key=os.getenv("DASHSCOPE_API_KEY"),
    base_url=os.getenv("DASHSCOPE_BASE_URL")
)

# dataclass会自动创建init、repr、eq方法,frozen能够保证对象初始化之后不能修改
@dataclass(frozen=True)
class Context:
    user_id: int
    user_permissions: str

# 引入装饰器
# 从 LangChain 的 Agent 中间件模块中导入以下组件:

from langchain.agents.middleware import (
    before_agent,      # 装饰器:在 Agent 开始执行前触发一次。适合初始化、权限校验、日志、修改初始状态或强制跳转。
    after_agent,       # 装饰器:在 Agent 执行结束后触发一次。适合资源清理、最终日志、结果后处理等。
    before_model,      # 装饰器:每次调用模型前触发。适合修改消息、动态调整提示词、注入上下文等。
    after_model,       # 装饰器:每次模型返回后触发。适合输出校验、内容过滤、解析结果、记录日志等。
    wrap_model_call,   # 装饰器:包裹模型调用。可在模型调用前后插入逻辑,如重试、缓存、限流、监控、修改请求/响应。
    wrap_tool_call,    # 装饰器:包裹工具调用。可在工具执行前后插入逻辑,如参数校验、权限检查、重试、结果处理。
    ModelRequest,      # 类型:模型请求对象。通常包含 messages、tools、配置等,供 wrap_model_call 中读取或修改。
    ModelResponse,     # 类型:模型响应对象。通常包含生成消息、工具调用等,供 wrap_model_call 中读取或修改。
    AgentMiddleware,   # 基类:用于以类的方式定义中间件,可集中组织多个钩子方法(before/after/wrap 等)。
)

# 通过 before_agent 注解设置进入Agent之前调用的函数
# 它的 can_jump_to 参数表示当前函数可以跳转到什么节点中
# 通过返回 jump_to 这个字段来实现跳转
# can_jump_to是告诉LangChain可以跳转什么节点
# 返回 jump_to 来实现跳转
# 如果 不写 can_jump_to 那么 jump_to 会无效
# can_jump_to 有三个值,如下
# "tools"表示可以跳转到工具节点,进入Agent之前和之后不可以跳转tools
# "model"表示可以跳转到模型节点或进入模型之前的钩子
# "end"表示跳转到Agent末尾 或 第一个进入Agent之后的钩子,也就是用来结束Agent的执行
@before_agent(can_jump_to=[])
def manage_human_message_before_agent(state: AgentState, runtime: Runtime[Context]):
    """
    在Agent启动之前调用
    Runtime:用来传递全局变量(只是用来读取的内容-上下文的常量),还经常用来传递数据库连接池、日志对象一些配置信息
    """

    # 从后往前找第一个类别为 HumanMessage 的消息
    user_content = ""
    for message in reversed(state["messages"]):
        if isinstance(message, HumanMessage):
            user_content = message.content
    # 打印用户最新的问题
    print(f"在before_agent中,用户最新问题:{user_content}")
    # 1.处理用户敏感用词
    sensitive_words = ["TM", "TMD", "CNM", "挂了", "垃圾"]
    if any(word in user_content.upper() for word in sensitive_words):
        # 发现敏感词,直接构造一个 AI 响应,不再交给模型思考
        return {
            "messages": [AIMessage(content="检测到不当言论,请文明交流。")],
            "jump_to": "end"  # 直接结束这次对话
        }
    # 2.vip用户特殊处理
    # 获取用户权限
    user_permissions = runtime.context.user_permissions
    if user_permissions == "vip":
        # vip权限能够进行所有知识库的访问
        print("我是VIP用户,能够查看全部的内容")
    else:
        print("我是普通用户,能够查看部分的内容")

    return None

# 创建json格式验证
def repair_json_string(raw_str: str) -> str:
    # 这一行是去掉 markdown 代码块的标记,比如 ```json 和 ```
    # r"```json\s*|```" 这个正则里面有一个 |,表示或者
    # 前面部分 ```json\s* 匹配的是 ```json 后面跟着空白(空格、换行都算)
    # 后面部分 ```匹配的就是三个反引号
    # 把匹配到的这些内容都替换成空字符串 "",也就是删掉
    # 然后 .strip() 是去掉字符串最前面和最后面的空白
    # 比如原来字符串是 "```json\n{\"a\": 1}\n```"
    # 经过这一行就变成了 "{\"a\": 1}"
    raw_str = re.sub(r"```json\s*|```", "", raw_str).strip()

    # 这一行是去掉 json 里面多出来的逗号
    # 比如 {"a": 1,} 或者 [1, 2,] 这种最后多一个逗号的情况
    # r",\s*([}\]])" 这个正则里面,逗号 , 匹配一个逗号
    # \s* 匹配逗号后面的空白,空格换行都行
    # ([}\]]) 是一个捕获组,括号里面写的是 } 或者 ],所以它匹配 } 或 ]
    # 整个正则匹配到的内容是:逗号 + 空白 + } 或 ]
    # 替换成 r"\1",这里的 \1 代表的是第一个捕获组,也就是 ([}\]]) 这个括号匹配到的内容
    # 因为 ([}\]]) 这个括号里写的是 } 或者 ],所以 \1 就是 } 或者 ]
    # 这样替换后,逗号和空白就被删掉了,只剩下 } 或 ]
    # 比如 {"a": 1,} 整个正则匹配到 ",}",其中 ([}\]]) 匹配到 "}",替换成 \1 就是 "}",所以变成 {"a": 1}
    # 比如 [1, 2,] 整个正则匹配到 ",]",其中 ([}\]]) 匹配到 "]", 替换成 \1 就是 "]", 变成 [1, 2]
    raw_str = re.sub(r",\s*([}\]])", r"\1", raw_str)

    # ([{,]\s*)([a-zA-Z0-9_]+)(\s*:) 这个正则表达式可以分成三组
    # 第一组 ([{,]\s*) 它的作用是匹配文字中的 { 或 ,,后面可跟空白
    # 第二组 ([a-zA-Z0-9_]+) 它的作用是匹配所有大小写字母和数字,这里是验证json也就是匹配一个json的key名字
    # 第三组 (\s*:) 它的作用是匹配冒号和空格
    # 也就是被括号包括起来可以看做出一组,如 (这是一组匹配规则) 这样
    # 连起来就是匹配这样的内容 {jjjj :
    # 正则表达式匹配完成后按照 \1"\2"\3 方式替换
    # \1代表第一组的内容也就是 ([{,]\s*) 匹配出来的内容
    # \2代表第二组的内容也就是([a-zA-Z0-9_]+)
    # \3代表第三组的内容也就是(\s*:)
    # 注意\2被加了双引号,假设正则表达式匹配出了 {jjjj : 这样的内容,经过 \1"\2"\3 替换后就变成了 {"jjjj" : 这样
    # \1 的内容是{
    # \2的内容是jjjj
    # \3的内容是 :
    raw_str = re.sub(r"([{,]\s*)([a-zA-Z0-9_]+)(\s*:)", r'\1"\2"\3', raw_str)

    return raw_str

# 模型输出后
@after_model
def fix_json_structure(state: AgentState, runtime: Runtime[Context]):
    # 1. 获取模型最后一条回复
    last_message = state["messages"][-1]
    if not isinstance(last_message, AIMessage):
        return
    # 这里我们模拟模型返回错误的json
    # raw_content = last_message.content
    raw_content = """
        ```json
        {
          "user_id": "123",
          "action": "send_package",
          "items": ["book", "pen"],
        }
    """
    print(f"开始进行json格式修复:{raw_content}")
    try:
        # 解析json,如果解析失败会出错,出错就会执行except里面的逻辑
        json.loads(raw_content)
    except json.JSONDecodeError:
        # 修复json
        fixed_content = repair_json_string(raw_content)
        print(f"json格式修复完成:{fixed_content}")

        try:
            # 再次验证修复结果
            json.loads(fixed_content)
            # 【关键】写回消息对象
            last_message.content = fixed_content
            # 也可以记录一个标记位说明发生过修正
            last_message.additional_kwargs["is_fixed"] = True
        except Exception:
            # 如果修复后还是不行,可以抛出异常触发重试,或记录错误
            pass

    return {"messages": state["messages"]}

# 创建智能体
agent = create_agent(model=llm,
                     # 设置中间件
                     middleware=[manage_human_message_before_agent,
                                 fix_json_structure])
result = agent.invoke({"messages": [HumanMessage("你好,我是计算机王")]}, context=Context(user_permissions="vip", user_id=1))
# 获取AI回复
print(result["messages"][-1].content)
print(result)

相关推荐
xsd202411181 小时前
LingCast灵播:数字人直播技术架构全解析
人工智能
Data analyse4561 小时前
数据合规的敏感数据怎么识别?
前端·人工智能·数据分析
嘿嘿-661 小时前
GPT-6.1 Sol 实战:做一个可交互的 3D 汽车展厅
人工智能·gpt·3d·chatgpt·汽车
AI你一生一世1 小时前
当广告采集器成为大模型的“眼睛“:跨站上下文注入的架构与代价
人工智能·架构·大模型·rag·隐私安全·上下文注入·跨站追踪
数字化顾问1 小时前
(136页PPT)BCGIDG资本中国某省市场Geneva项目商业尽职调查项目报告(附下载方式)
大数据·人工智能
故七月1 小时前
GEO 工程实践:企业官网结构化改造,提升大模型实体采信度
大数据·人工智能·geo
释厄6231 小时前
物理发展史·量子力学=节点2·牛顿力学=节点3·宇宙的语言算法AI
人工智能·算法·microsoft
乃嘿仔1 小时前
AI 热点日报 · 2026-10-01
开发语言·人工智能·php
AAASilverwolf1 小时前
GPT-6 Sol 与 Luna 上线:Astra 的能力下放,价格却降了 50%?
人工智能·gpt·api