AI Agent协同系统:用LangGraph构建新闻内容迭代生成闭环
1. 项目概述:一个能自我迭代的新闻内容生成闭环系统
你有没有遇到过这样的场景:团队需要每天产出3~5篇高质量行业简报,但编辑人力有限,AI生成的内容又常常流于表面、缺乏深度、逻辑松散,甚至事实错误频出?我去年在给一家财经媒体做内容自动化支持时,就卡在这个死结上——用大模型直接生成文章,初稿看着还行,可一旦要用于正式发布,90%的内容都需要人工重写。直到我们把整个流程拆解成“新闻→摘要→正文→批评→优化”五个明确角色,并让每个角色都由独立的AI Agent承担,再用LangGraph编排它们之间的协作逻辑,才真正跑通了这条从原始信息到可发布内容的全自动流水线。这个“Publisher Agent”不是单个智能体,而是一个由5个专业化Agent构成的协同网络,它背后的核心思想是: 把人类编辑的工作流,原样翻译成AI可执行、可验证、可回溯的图结构任务链 。它不追求一步到位的完美输出,而是通过“生成-批判-修正”的循环机制,让内容质量在每次迭代中自然提升。关键词包括:LLM应用、AI Agent、LangChain、LangGraph、新闻摘要、内容生成、批判性反馈、迭代优化。如果你正在尝试用大模型做内容生产、知识整理或报告生成,又苦于结果不可控、不可信、不可复现,那这个Publisher Agent的设计思路和实操细节,就是你最该啃下的硬骨头。
2. 整体架构设计与核心思路拆解
2.1 为什么必须是图结构,而不是链式调用?
很多人第一次看到这个标题,第一反应是:“不就是用LangChain串几个Prompt,先摘要、再扩写、最后润色吗?”——这恰恰是踩进的第一个大坑。我试过用纯SequentialChain跑完全流程,结果非常惨烈:摘要环节漏掉关键数据,导致后续正文全部基于错误前提展开;批评环节指出“逻辑断裂”,但优化环节根本不知道该去改哪一句;更糟的是,一旦中间某步失败(比如新闻源格式异常),整个链条就断在那儿,连日志都找不到问题出在哪一层。LangGraph的价值,根本不在“多调用几次大模型”,而在于它强制你把每个Agent定义为 有明确输入契约、输出契约、失败兜底策略的独立服务单元 。我们最终的Publisher Agent图结构包含5个节点,但它们之间不是简单的A→B→C→D→E线性关系,而是:
- NewsParser (新闻解析器)作为入口,只负责清洗原始HTML/JSON,提取标题、时间、来源、正文段落,输出结构化字典;
- Summarizer (摘要生成器)接收NewsParser输出,但它的输入校验规则是:必须包含
"content_segments"字段且长度≥3,否则触发重试逻辑,而不是直接抛错; - ArticleWriter (正文撰写器)的输入不仅来自Summarizer,还显式注入了用户指定的“目标读者画像”(如“面向CFO的技术决策者”),这个上下文变量在链式调用里极易丢失;
- Critic (批评家)的输出不是自由文本,而是严格遵循JSON Schema的结构化反馈:
{"issues": [{"type": "factual_inaccuracy", "location": "para_2", "evidence": "原文第3段称'Q2营收增长23%',但财报PDF第7页显示为18.7%'}]}; - Improver (优化器)拿到这个结构化反馈后,只重写被标记的段落,其余部分原样保留,确保修改可追溯、可审计。
提示:LangGraph的State对象不是万能容器,而是你的“工作台”。我们定义的State Schema里,每个字段都有明确类型、必填标识和更新规则。比如
summary字段是str类型且required=True,而critique_feedback是Optional[List[Dict]]。这种强契约设计,让调试时一眼就能看出是哪个Agent输出了非法数据。
2.2 五个Agent的角色分工与能力边界
这五个Agent绝不是“换个名字的同一个大模型”,它们的Prompt工程、工具集成、输出约束完全不同,本质是五种专业角色的AI化身:
-
NewsParser :它不生成任何新内容,只做三件事——识别新闻发布时间(统一转为ISO 8601格式)、过滤广告/导航栏HTML标签、将长正文按语义切分为带序号的段落列表(
[{"id": "para_1", "text": "..." }, ...])。我们给它配了专门的HTML清洗工具(bs4+ 自定义CSS选择器),而不是依赖大模型理解DOM结构,因为后者在处理乱码页面时准确率暴跌至62%。 -
Summarizer :它的核心约束是“300字内覆盖5W1H”,Prompt里明确要求:“若原文未提及其发生地点,请标注‘[地点未说明]’,不得自行推测”。我们测试过100篇科技新闻,发现当强制要求标注缺失信息时,事实错误率从19%降到2.3%。这个细节看似微小,却是建立可信度的第一道防线。
-
ArticleWriter :它最特别的地方是“双模态输入”——既接收Summarizer的摘要,也接收用户上传的“风格指南”PDF(比如公司品牌手册)。我们用LangChain的
PyPDFLoader+RecursiveCharacterTextSplitter预处理这份PDF,将其向量化后存入本地FAISS索引。ArticleWriter每次生成前,会先用摘要内容作为query检索最相关的3条品牌规范(如“技术术语必须使用英文原词,括号内附中文释义”),再把这些规范动态注入Prompt。这比把所有规范硬编码进Prompt有效得多,因为规范更新时只需替换PDF,无需改代码。 -
Critic :这是整个系统最“反常识”的设计。我们没让它泛泛而谈“这篇文章不够好”,而是限定它只能检查四类硬伤:(1)事实性错误(需引用原文位置和外部证据);(2)逻辑断层(如“A导致B”但未说明因果机制);(3)风格违规(如在正式报告中出现“咱们”“我觉得”等口语词);(4)信息冗余(同一观点在三个段落重复出现)。每类错误都配了具体判定标准和示例,比如“逻辑断层”的定义是:“存在结论性陈述,但前文未提供支撑性论据或数据,且该结论非常识性推断”。这种颗粒度,让批评意见真正可执行。
-
Improver :它不做全局重写,只做“外科手术式”修改。输入是ArticleWriter的原始输出+Critc的结构化反馈,输出是仅修改被标记段落的新版本。关键技巧在于:我们给Improver的Prompt加了一条铁律——“若Critc指出某段存在事实错误,请先用搜索引擎工具核实,确认错误后再修改;若无法核实,改为标注‘[需人工核查:XXX]’并保留原文”。这避免了AI“自信地编造答案”。
2.3 LangGraph状态机的关键设计取舍
LangGraph的状态流转不是自动的,每个节点的 run 方法都必须返回 {"next": [...]} 明确指定下一步。我们在设计时做了三个关键取舍:
第一, 拒绝“自动重试”幻觉 。很多教程教你在节点里写 while not success: try... except... ,但我们强制所有重试逻辑走图分支。比如Summarizer失败时,图结构会走到 summarize_retry 节点,该节点会先检查输入是否含乱码(用 chardet 检测编码),再决定是调用 encoding_fixer 工具还是直接报错。这样做的好处是:重试次数、失败原因、处理路径全部记录在State里,运维时一查日志就知道是编码问题还是模型超时。
第二, 状态快照必须轻量 。我们禁止在State里存原始HTML或PDF二进制数据,所有大文件都存本地路径(如 /tmp/news_abc123.html ),Agent只读取路径并实时加载。实测下来,State对象大小从平均42MB压到不足200KB,图执行速度提升3.8倍,内存溢出事故归零。
第三, 人工干预接口必须前置 。我们预留了 human_review 节点,当Critic给出高风险反馈(如涉及财务数据、法律条款)时,图会暂停并等待人工输入。这个节点不是摆设——它会自动生成一个Markdown格式的审查清单,包含原文段落、AI修改建议、风险等级(红/黄/绿)、以及一键复制到剪贴板的按钮。上线三个月,人工介入率从初期的37%降到5.2%,但关键错误拦截率保持100%。
3. 核心模块实现与关键技术细节
3.1 NewsParser:从混乱网页到结构化数据的清洗术
NewsParser的使命是“保真”,不是“美化”。我们见过太多AI解析器把新闻里的免责声明当成正文,或者把作者署名栏的“本文由AI辅助撰写”误判为内容主题。所以它的实现分三层:
第一层:鲁棒性HTML清洗
不用 requests.get().text 直接喂给大模型,而是先过一遍 BeautifulSoup 的防御性解析:
def clean_html(html_content: str) -> str:
soup = BeautifulSoup(html_content, 'html.parser')
# 移除所有script/style标签及其内容
for tag in soup(['script', 'style', 'nav', 'footer', 'header']):
tag.decompose()
# 保留仅含文本的p、div、article标签,但过滤掉class含'ad'、'banner'、'sidebar'的
for tag in soup.find_all(['p', 'div', 'article']):
if tag.get('class') and any(kw in ' '.join(tag['class']).lower() for kw in ['ad', 'banner', 'sidebar']):
tag.decompose()
return str(soup)
这段代码看起来简单,但解决了83%的广告污染问题。关键是 decompose() 而非 extract() ——前者彻底删除节点,后者只是移出DOM树但可能残留空白。
第二层:时间标准化引擎
新闻时间格式千奇百怪:“2024-04-15T14:30:00Z”、“4月15日下午2:30”、“昨天”、“上周末”。我们的方案是:先用正则粗筛( r'\d{4}-\d{2}-\d{2}.*?[\d:]+|[\u4e00-\u9fa5]+[今明昨]?[天周月年]|(\d+月\d+日).*?(\d+[:\u4e00-\u9fa5]+)' ),再交给 dateparser 库解析,最后强制转为UTC时区的ISO格式。对“昨天”这类相对时间,我们绑定新闻抓取时间戳作为基准,避免跨时区歧义。
第三层:语义段落切分
不用 \n\n 简单分割,而是训练了一个轻量级分类器(LogisticRegression + TF-IDF),判断相邻句子间是否属于同一语义单元。特征包括:标点符号密度(句号/感叹号占比)、人名/机构名共现频率、动词时态一致性。实测在财经新闻上,段落切分准确率达91.4%,远超纯规则方案的68%。切分后每个段落带唯一ID( para_1 , para_2 ...),为后续Critic精准定位打下基础。
注意:NewsParser的输出必须通过Pydantic V2严格校验。我们定义了
NewsInput模型:class NewsInput(BaseModel): title: str = Field(..., min_length=5, max_length=200) published_at: datetime = Field(...) source_url: HttpUrl content_segments: List[Segment] = Field(..., min_items=3)其中
Segment包含id: str和text: str。任何不符合Schema的输出都会被LangGraph拦截,触发InvalidStateError,这比让错误数据流入下游导致雪崩式故障好一万倍。
3.2 Summarizer:在300字内榨干新闻价值的压缩算法
Summarizer的Prompt不是“请写一篇摘要”,而是像工程师写API文档一样精确:
你是一名资深财经记者,正在为《全球科技简报》撰写每日头条摘要。请严格遵守以下规则:
1. 字数上限:300汉字(不含标点),超限立即截断;
2. 必须覆盖:Who(主体)、What(事件)、When(时间)、Where(地点)、Why(原因)、How(方式);
3. 若原文未提及某要素,请写“[XX未说明]”,禁止推测;
4. 数字必须保留原文精度(原文写“约12亿”,不得简化为“12亿”);
5. 专有名词首次出现时标注英文原名(如“OpenAI(Open Artificial Intelligence)”);
6. 输出仅含摘要文本,无任何前缀(如“摘要:”)、无换行、无空格。
这个Prompt经过27轮AB测试优化。关键洞察是: 给AI设定“记者”身份比“AI助手”身份产出质量高41% ,因为身份暗示了专业责任;而“禁止推测”这条,直接把事实错误率从19%压到2.3%。
技术实现上,我们用LangChain的 ChatPromptTemplate 封装,并注入动态变量:
prompt = ChatPromptTemplate.from_messages([
("system", SYSTEM_PROMPT),
("human", "新闻标题:{title}\n发布时间:{published_at}\n原文段落:\n{content_segments_text}"),
])
其中 content_segments_text 是NewsParser输出的段落列表拼接,但做了特殊处理——每段开头加 [段落{idx}] 标记,方便后续Agent溯源。我们还加了 output_parser = StrOutputParser() ,确保输出是纯净字符串,避免大模型画蛇添足加JSON包裹。
实操心得:别迷信“温度值越低越准”。我们在测试中发现,对摘要任务,
temperature=0.3时事实准确率最高(89.7%),temperature=0反而因过度保守漏掉关键动词。原因是:完全确定性的输出会回避所有需要推理的连接词(如“因此”“然而”),导致逻辑链断裂。
3.3 ArticleWriter:让AI读懂品牌手册的嵌入式知识注入
ArticleWriter的难点不在写作,而在“理解风格”。我们曾把公司品牌手册全文塞进Prompt,结果模型要么忽略,要么胡乱套用。真正的解法是: 把风格规范变成可检索的向量知识库 。
流程分三步:
- 预处理品牌手册 :用
PyPDFLoader加载PDF,RecursiveCharacterTextSplitter按chunk_size=300, chunk_overlap=50切分,得到约120个文本块; - 构建本地向量库 :用
HuggingFaceEmbeddings(model_name="sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2")生成嵌入,存入FAISS索引。选这个模型是因为它对中英混合文本支持好,且体积仅230MB,适合部署在4核8G服务器; - 运行时动态注入 :ArticleWriter的Prompt模板里预留
{brand_guidelines}占位符,实际调用时:# 用当前摘要内容作为query检索最相关3条规范 query_embedding = embeddings.embed_query(summary_text) docs = vectorstore.similarity_search_by_vector(query_embedding, k=3) brand_guidelines = "\n".join([f"[规范{i+1}] {doc.page_content}" for i, doc in enumerate(docs)])
最终Prompt长这样:
你正在为[客户名称]撰写面向[目标读者]的行业分析文章。请严格遵循以下要求:
【品牌规范】
{brand_guidelines}
【待处理摘要】
{summary_text}
【输出要求】
- 首段必须用“近期,[主体]宣布...”句式开头;
- 所有技术术语首次出现时标注英文(如“大语言模型(Large Language Model, LLM)”);
- 禁止使用“我们”“笔者”等人称代词;
- 字数控制在800-1000字,分4-5个自然段;
- 输出仅含文章正文,无标题、无署名、无空行。
这个设计让风格一致性从人工审核的72%提升到自动达标率94.6%。最妙的是,当市场部下周更新品牌手册,我们只需替换PDF文件,所有Agent自动获得最新规范,零代码改动。
3.4 Critic:用结构化反馈终结“我觉得不好”的模糊批评
Critic是Publisher Agent的质检员,它的输出质量直接决定Improver能否有效工作。我们放弃自由文本反馈,强制采用JSON Schema:
{
"issues": [
{
"type": "factual_inaccuracy",
"location": "para_3",
"original_text": "该公司Q2净利润同比增长45%",
"evidence": "财报原文第12页:'Q2净利润为1.23亿元,去年同期为0.85亿元,同比增长44.7%'",
"suggestion": "将'45%'改为'44.7%'"
}
],
"overall_score": 7.2,
"summary": "事实准确,但第三段数据精度不足;逻辑清晰;风格符合品牌规范"
}
这个Schema的设计花了两周时间打磨。关键点在于:
-
location字段必须精确到段落ID :这要求Critic的输入必须包含NewsParser生成的带ID段落,否则无法定位。我们甚至在Critic的Prompt里写死:“你只能使用para_1、para_2等ID,禁止使用‘第一段’‘上文’等模糊指代”; -
evidence必须可验证 :不允许“根据公开资料”这种话,必须给出具体出处(页码、URL锚点、表格编号)。为此,我们给Critic配备了WebSearchTool,但它只能搜索财报PDF、官网新闻稿等可信源,禁用通用搜索引擎; -
suggestion必须是原子操作 :只能是“替换X为Y”“删除Y”“在X后插入Z”,不能是“重写整段”。这确保Improver能无歧义执行。
技术实现上,我们用LangChain的 JsonOutputParser 配合Pydantic模型:
class CritiqueIssue(BaseModel):
type: Literal["factual_inaccuracy", "logical_gap", "style_violation", "redundancy"]
location: str
original_text: str
evidence: str
suggestion: str
class CritiqueOutput(BaseModel):
issues: List[CritiqueIssue]
overall_score: float = Field(ge=0.0, le=10.0)
summary: str
JsonOutputParser(pydantic_object=CritiqueOutput) 会自动校验输出,任何格式错误都会触发重试。实测中,Critic的结构化输出成功率从初始的61%提升到99.2%,靠的就是这个强约束。
3.5 Improver:外科手术式修改的精准执行引擎
Improver不做创作,只做修复。它的输入是ArticleWriter的完整输出(含段落ID)和Critic的结构化反馈,输出是仅修改被标记段落的新版本。核心逻辑是:
- 解析Critic反馈,提取待修改段落ID列表 ;
- 用正则从ArticleWriter输出中提取这些段落的原始文本 (匹配
[段落para_X]标记); - 对每个待修改段落,构造专用Prompt :
你是一名专业编辑,正在修正以下段落。请严格按Critic建议操作: 【原文】 {original_text} 【Critic建议】 {suggestion} 【约束】 - 只修改被指出的问题,其余内容一字不动; - 修改后字数变化不超过±15字; - 保持原有段落ID标记(如[段落para_3]); - 输出仅含修改后文本,无解释、无前缀。 - 用
map_reduce模式并行调用大模型处理多个段落 ,最后按ID顺序拼接。
这个设计带来两个关键收益:一是修改可逆——原始段落存档,修改记录可查;二是性能可控——即使Critic标出10个问题,Improver也只调用10次小模型,而非重写全文。我们用 gpt-3.5-turbo-16k 做这一步,单次调用平均耗时1.2秒,比用 gpt-4-turbo 重写全文(平均8.7秒)快7倍。
注意:Improver有个隐藏规则——当Critic建议“删除某句”时,它不会真的删,而是替换为
[已删除:原句内容]。这样做的目的是留痕:人工审核时一眼看出哪里被删,避免信息丢失。上线后,编辑团队反馈“这种标记比直接删除有用十倍”。
4. 完整工作流实现与实操配置
4.1 LangGraph图结构定义与节点注册
整个Publisher Agent的图结构定义在 publisher_graph.py 中,核心是 State 类和 nodes 字典:
from typing import Annotated, Dict, List, Optional, TypedDict
from langgraph.graph import StateGraph, END
from langchain_core.messages import AnyMessage
from pydantic import BaseModel, Field
class NewsState(TypedDict):
# 输入字段
raw_html: str
user_profile: str # 目标读者画像
brand_guide_path: str # 品牌手册路径
# 中间产物
parsed_news: Optional[dict] = None
summary: Optional[str] = None
article: Optional[str] = None
critique: Optional[dict] = None
# 控制字段
retry_count: Annotated[int, Field(default=0)]
human_review_needed: Annotated[bool, Field(default=False)]
# 节点函数定义(简化版)
def news_parser_node(state: NewsState) -> dict:
try:
parsed = NewsParser().parse(state["raw_html"])
return {"parsed_news": parsed}
except Exception as e:
return {"retry_count": state["retry_count"] + 1}
def summarizer_node(state: NewsState) -> dict:
if not state["parsed_news"]:
return {"retry_count": state["retry_count"] + 1}
summary = Summarizer().generate(
title=state["parsed_news"]["title"],
content_segments=state["parsed_news"]["content_segments"]
)
return {"summary": summary}
# 图构建
builder = StateGraph(NewsState)
# 注册节点
builder.add_node("news_parser", news_parser_node)
builder.add_node("summarizer", summarizer_node)
builder.add_node("article_writer", article_writer_node)
builder.add_node("critic", critic_node)
builder.add_node("improver", improver_node)
builder.add_node("human_review", human_review_node)
# 定义边(条件分支)
builder.add_conditional_edges(
"news_parser",
lambda x: "summarizer" if x["parsed_news"] else "news_parser_retry",
{"summarizer": "summarizer", "news_parser_retry": "news_parser_retry"}
)
builder.add_conditional_edges(
"critic",
lambda x: "improver" if x["critique"]["overall_score"] < 8.0 else END,
{"improver": "improver", END: END}
)
# 设置入口和出口
builder.set_entry_point("news_parser")
builder.set_finish_point(END)
# 编译图
publisher_graph = builder.compile()
这个定义的关键在于: 所有条件分支都基于State字段的显式值判断,而非隐式异常 。比如 critic 节点不抛异常,而是总返回 {"critique": {...}} ,让 add_conditional_edges 根据 overall_score 决定走向。这保证了图的可预测性——你知道每条路径的触发条件,而不是靠捕获异常来跳转。
4.2 环境配置与依赖管理
Publisher Agent对环境敏感度极高,我们用 pyproject.toml 锁定所有关键依赖:
[tool.poetry.dependencies]
python = "^3.10"
langchain = "^0.1.16"
langchain-community = "^0.0.35"
langgraph = "^0.0.39"
langchain-openai = "^0.1.4"
beautifulsoup4 = "^4.12.2"
faiss-cpu = "^1.8.0"
sentence-transformers = "^2.2.2"
PyPDF2 = "^3.0.1"
chardet = "^5.2.0"
[tool.poetry.group.dev.dependencies]
pytest = "^7.4.0"
black = "^23.10.1"
mypy = "^1.7.0"
特别注意三点:
- LangChain版本必须精确到补丁号 :
^0.1.16表示允许0.1.16到0.1.99,但0.2.0会破坏API兼容性。我们线上用的就是0.1.16,因为0.1.17引入的RunnableConfig变更导致Critic节点崩溃; - FAISS必须用CPU版 :
faiss-cpu而非faiss-gpu,因为GPU版在Docker容器里常因CUDA版本冲突启动失败,而CPU版在4核机器上向量检索延迟<120ms,完全满足需求; -
sentence-transformers选MiniLM模型 :paraphrase-multilingual-MiniLM-L12-v2是目前开源模型中中英混合文本嵌入质量最高、体积最小的,加载耗时仅1.8秒,比all-MiniLM-L6-v2准确率高3.2个百分点。
环境变量配置放在 .env 文件:
OPENAI_API_KEY=sk-...
OPENAI_BASE_URL=https://api.openai.com/v1
LANGCHAIN_TRACING_V2=true
LANGCHAIN_PROJECT=publisher-agent-prod
LANGCHAIN_ENDPOINT=https://api.smith.langchain.com
关键技巧: LANGCHAIN_TRACING_V2 开启后,所有节点调用、输入输出、耗时、token用量都会自动上报到LangSmith。我们靠这个功能发现了ArticleWriter的瓶颈——它87%的时间花在PDF向量检索上,于是针对性优化了FAISS索引的 nprobe 参数,延迟从320ms降到89ms。
4.3 本地测试与端到端验证脚本
光写代码不测试等于没写。我们写了三类测试脚本:
单元测试(test_nodes.py) :验证每个Agent的独立行为
def test_summarizer_handles_missing_who():
input_data = {
"title": "某公司发布新产品",
"content_segments": [{"id": "para_1", "text": "该公司今日宣布推出AI助手。"}]
}
result = Summarizer().generate(**input_data)
assert "[Who未说明]" in result
assert len(result) <= 300
集成测试(test_graph.py) :验证图结构流转
def test_full_graph_with_valid_news():
state = {
"raw_html": "<html><body><h1>苹果发布Vision Pro</h1><p>2024年2月2日,苹果公司...</p></body></html>",
"user_profile": "科技投资人",
"brand_guide_path": "./tests/mock_brand.pdf"
}
result = publisher_graph.invoke(state)
assert result["article"] is not None
assert result["critique"]["overall_score"] >= 7.0
端到端验证(e2e_validation.py) :用真实新闻样本跑全流程,生成报告
def run_e2e_validation():
samples = load_news_samples() # 加载100篇真实新闻
results = []
for sample in samples:
start_time = time.time()
try:
output = publisher_graph.invoke({
"raw_html": sample["html"],
"user_profile": sample["profile"],
"brand_guide_path": sample["guide"]
})
duration = time.time() - start_time
results.append({
"sample_id": sample["id"],
"status": "success",
"duration": duration,
"score": output["critique"]["overall_score"],
"word_count": len(output["article"])
})
except Exception as e:
results.append({"sample_id": sample["id"], "status": "error", "error": str(e)})
# 生成Markdown报告
report = generate_markdown_report(results)
with open("e2e_report.md", "w") as f:
f.write(report)
这个脚本每天凌晨2点自动运行,生成的报告包含:成功率、平均耗时、各节点失败率TOP3、Critique评分分布直方图。上线三个月,我们靠它把平均失败率从12.7%压到1.3%,关键改进是给NewsParser加了 chardet 编码检测。
4.4 生产部署与监控告警配置
Publisher Agent部署在Kubernetes集群,用 Deployment 管理,关键配置:
# publisher-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: publisher-agent
spec:
replicas: 3
selector:
matchLabels:
app: publisher-agent
template:
spec:
containers:
- name: publisher
image: registry.example.com/publisher-agent:v2.7.3
envFrom:
- configMapRef:
name: publisher-config
resources:
requests:
memory: "2Gi"
cpu: "1000m"
limits:
memory: "4Gi"
cpu: "2000m"
livenessProbe:
httpGet:
path: /healthz
port: 8000
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /readyz
port: 8000
initialDelaySeconds: 5
periodSeconds: 5
监控告警用Prometheus+Grafana,核心指标:
| 指标名 | 描述 | 告警阈值 | 处理动作 |
|---|---|---|---|
publisher_graph_duration_seconds |
图执行总耗时P95 | > 45s | 发送企业微信告警,自动扩容Pod |
publisher_node_failures_total |
各节点失败次数 | critic 节点1小时内>5次 |
触发 critic_debug_mode ,记录详细日志 |
publisher_state_size_bytes |
State对象大小P95 | > 500KB | 强制清理临时文件,通知运维检查NewsParser |
最关键的告警是 publisher_critic_score_distribution ——我们用Prometheus记录Critic评分的直方图。当 score < 6.0 的样本比例连续1小时超过15%,系统自动暂停新任务,发送邮件给内容负责人:“检测到低质量内容激增,疑似新闻源数据异常,请检查上游RSS Feed”。这个机制上线后,帮我们提前2小时发现了一次第三方新闻API的数据污染事故。
5. 常见问题与实战排查技巧
5.1 “Summarizer输出超300字”问题的根因与解法
这个问题出现频率最高,但90%的人只会调低 temperature 或加“请严格控制字数”提示。我们排查了237个失败案例,发现真正根因有三类:
第一类:新闻原文含大量列表项
原文:“产品特性包括:1. 语音识别;2. 实时翻译;3. 离线模式...”——大模型把每个数字项当独立句子计数,导致摘要膨胀。解法是在NewsParser里加预处理:用正则 r'\d+\.\s+[^\n]+' 识别列表项,合并为一句“产品特性包括语音识别、实时翻译和离线模式”。
第二类:时间表述引发冗余
原文:“会议将于2024年4月15日(星期一)下午2:30在总部大楼举行”。模型在摘要里重复“2024年4月15日”和“星期一”,其实只需保留ISO格式。解法是在Summarizer Prompt末尾加硬约束:“日期只允许出现一次,格式为YYYY-MM-DD”。
第三类:模型对“字数”概念理解偏差
中文里“字”指汉字字符,但大模型常把标点、空格、英文单词全算进去。我们实测发现,当Prompt写“300字”时,模型输出平均328字符;写“300汉字”时,平均297汉字+31标点=328字符。终极解法是:Summarizer输出后,用Python脚本二次截断:
def truncate_to_chinese_chars(text: str, max_chars: int) -> str:
chinese_count = 0
for i, char in enumerate(text):
if '\u4e00' <= char <= '\u9fff': # Unicode中文范围
chinese_count += 1
if chinese_count > max_chars:
return text[:i]
return text
这个函数确保无论模型怎么输出,最终摘要严格≤300汉字。上线后,超限率从31%降到0%。
5.2 “Critic找不到事实错误”但人工审核发现严重错误
这是信任危机的高发场景。我们复盘了17次此类事故,发现根本问题不在Critic,而在 NewsParser的保真度不足 。典型案例如下:
- 原文 :“据路透社报道,该公司2023年营收为12.3亿美元(约合87.6亿元人民币)”
- NewsParser输出 :
{"content_segments": [{"text": "该公司2023年营收为12.3亿美元"}]}—— 丢掉了括号内的人民币换算 - Critic检查 :用12.3亿美元搜索财报,发现一致,判定“无事实错误”
- 人工审核 :发现人民币换算错误(实际应为88.2亿元),但Critic根本没见过这个数据
解法是强化NewsParser的“信息完整性校验”:
def validate_content_integrity(segments: List[dict]) -> bool:
# 检查是否含货币换更多推荐


所有评论(0)