How to Give Your SOC AI Agent Memory: Closing the Loop with Analyst Feedback

A three-layer memory architecture — entity reputation, rule priors, and an investigation knowledge base — that lets your SOC AI agent get measurably better after every analyst verdict.

An AI triage agent that makes the same mistake on Tuesday that it made on Monday isn’t learning. It’s just guessing faster.

We wrote about building an LLM-powered triage pipeline a few weeks ago. That post covered the request/response shape: alert in, verdict out. What it didn’t cover is the part that actually determines whether the agent is worth keeping around after 90 days — what happens to the analyst’s verdict after the agent gets it wrong.

Most SOC AI deployments treat every alert as a cold start. The model sees the event, maybe a lookback window of raw logs, and produces a verdict with zero awareness that an analyst closed an identical alert from the same IP as a false positive last week. That’s not a model problem. It’s a missing memory layer.

The short version

LayerWhat it storesWhat it’s for
Entity memoryReputation and history per actor ARN / IP / roleInject prior context into triage before the model sees the new alert
Rule memoryTP/FP rates and extracted patterns per detection ruleGive the model a base rate instead of a 50/50 guess
Investigation KBCompressed summaries of closed casesRetrieve similar past investigations for the current one

None of this requires a vector database or a fine-tuned model. It’s three tables in your event store, a write path that fires after human review, and a read path that fires before the next triage call.

Why stateless triage plateaus

A triage agent with no memory has exactly one signal: the current alert. It can reason well about that single event, but it can’t answer the questions an experienced L2 analyst answers instantly:

  • “Has this actor done this before, and was it ever confirmed malicious?”
  • “Does this rule normally fire true or false for this kind of source?”
  • “Have we seen this exact pattern closed as a false positive already?”

Without those answers, the model either over-escalates (alert fatigue, the thing you’re trying to avoid) or under-escalates (the thing that gets you breached). Both failure modes get worse over time if nothing is feeding corrections back in. The fix is the same one human SOC teams use: institutional memory, backed by a mandatory feedback loop instead of tribal knowledge that leaves when an analyst does.

Layer 1: entity memory

One row per actor ARN, source IP, or IAM role, keyed on (entity_type, entity_value) and structured so every update overwrites the latest snapshot instead of accumulating duplicate rows:

entity_memory

  entity_type          string        "actor_arn" | "source_ip" | "role"
  entity_value         string
  total_alerts         integer
  confirmed_tp_count   integer
  confirmed_fp_count   integer
  reputation_score     float         0.0 clean -> 1.0 highly suspicious
  knowledge_summary    text          AI-generated, updated after each closed case
  mitre_tactics_seen   list<string>
  updated_at           timestamp

The reputation_score is a weighted blend, not an LLM output — keep the number deterministic so it’s auditable:

tp_rate    = tp_count / (tp_count + fp_count) if (tp_count + fp_count) else 0.5
sev_score  = (critical_count * 1.0 + high_count * 0.6) / total_alerts
reputation = round(0.6 * tp_rate + 0.4 * sev_score, 3)

knowledge_summary is the one field the model does write — a compressed, ≤200-word freetext description of the entity’s behavior, regenerated after every closed case and injected verbatim into the next triage prompt for that actor. That’s the mechanism that lets the agent say “this service account normally does bulk S3 reads at 02:00 UTC as part of a nightly ETL job” instead of flagging it cold every night.

Layer 2: rule memory

Entity memory tells you about the who. Rule memory tells you about the rule — every detection has a real-world TP/FP rate, and the model should know it before forming an opinion:

rule_memory   (one row per detection rule, latest snapshot wins)

  rule_name        string
  total_fired      integer
  tp_count         integer
  fp_count         integer
  avg_confidence   float
  fp_patterns      list<string>   AI-extracted
  tp_patterns      list<string>   AI-extracted
  last_updated     timestamp

A rule that’s 95% false positive should start every triage with the model already skeptical. A rule that’s 90% true positive should start it already leaning to escalate. This is the single highest-leverage table in the whole system — it turns “detection engineering tunes rules” from a quarterly review into a number the AI agent consults on every alert.

The fp_patterns / tp_patterns fields are short, reusable, human-readable conditions extracted by the model itself from closed cases — things like "source IP belongs to known CI/CD range" or "API call outside business hours from unfamiliar region". Cap them at a handful per rule (keep the last 10, deduplicated) so the prompt injection stays small and doesn’t drift into noise.

Layer 3: investigation knowledge base

One row per closed investigation, ordered by (rule_name, closed_at) so “give me the last 3 closed cases for this rule” is a cheap, index-friendly lookup:

investigation_kb

  id                       identifier
  alert_id                 string
  rule_name                string
  entity_value             string
  verdict                  string         true_positive | false_positive | inconclusive
  blast_radius             string
  mitre_techniques         list<string>
  case_summary             text           AI-generated, <=300 words
  distinguishing_signals   list<string>   what actually settled the verdict
  closed_at                timestamp

This is deliberately not a vector store. For most SOC teams, “last 3 closed cases for the same rule” beats semantic similarity search on cost and latency, and it’s trivial to reason about when an analyst asks “why did the agent say that?” If you’re running high alert volume across dozens of rule variants, swap the retrieval for embeddings — but don’t reach for that complexity before you’ve proven the plain SQL version isn’t enough.

The write path: consolidation after every closed case

All three tables update from one place: a consolidation step that runs when a case closes, whether that’s a human verdict or an auto-suppress. This is the part teams skip, and it’s the part that actually makes the system learn.

Alert fires
  -> triage agent reads entity + rule memory, forms verdict
  -> analyst reviews, sets human_verdict (true_positive | false_positive | inconclusive)
  -> case closed event
  -> consolidation step:
       1. generate updated entity knowledge_summary (a small, cheap model is enough)
       2. extract TP/FP distinguishing patterns for the rule
       3. write entity_memory, rule_memory, investigation_kb
  -> next alert for the same actor/rule reads the updated memory

The consolidation agent should run on a small, cheap model — it’s summarization and pattern extraction, not judgment. Save the larger model for the investigation itself. Two prompts do the work:

ENTITY_SUMMARY_PROMPT:
  Given the history of alerts and investigations for this actor/IP,
  produce a ≤200-word summary a future analyst (or the agent itself)
  can use to quickly assess risk. Focus on behavioral patterns, confirmed
  techniques, and whether past alerts were real or noise, and why.

PATTERN_EXTRACTION_PROMPT:
  Given a closed investigation, return JSON with fp_patterns and
  tp_patterns — concrete, reusable conditions that distinguished this
  case as true or false positive. Max 3 patterns per list.

Both prompts return short, structured text specifically because it gets re-injected into future prompts. A knowledge summary that grows unbounded eventually crowds out the actual alert context — cap it, regenerate it (not append to it) on every update, and treat prompt budget as a resource you’re actively managing.

The read path: context before judgment, not after

Memory only works if it’s assembled before the model forms an opinion, not fetched afterward to justify one. Build a single context object per triage call:

context = {
    "actor_history": {
        "reputation_score": 0.82,
        "confirmed_tp_count": 4,
        "confirmed_fp_count": 1,
        "knowledge_summary": "Service account tied to nightly ETL job...",
    },
    "rule_history": {
        "tp_rate": 0.12,
        "fp_patterns": ["source IP in known CI/CD range", "..."],
    },
    "similar_past_cases": [...],  # last 3 closed cases for this rule
}

Inject it into the system or user prompt ahead of the raw alert payload. The model should see “this rule is normally 88% noise, and this specific actor has a clean track record” before it sees the alert details — same order a good human analyst works in.

One thing worth calling out explicitly: everything in this context object except the numeric fields is model-generated text stored and replayed back into a future prompt. That’s a second-order prompt injection surface — if knowledge_summary or case_summary ever gets contaminated by an attacker-controlled log field flowing through the investigation, it re-enters every future triage for that entity. Apply the same untrusted-input handling from defending AI agents against prompt injection to what you write into memory, not just what you read out of alerts.

What this buys you

Before memoryAfter memory
Every alert triaged coldPrior TP/FP rate and actor history injected up front
Analyst re-explains the same FP pattern every weekPattern extracted once, applied automatically going forward
No way to answer “why did the AI escalate this”case_summary and distinguishing_signals are queryable
Model quality is static after deploymentModel output quality tracks the volume of reviewed cases

The tradeoff is operational, not architectural: this only works if human verdicts are actually being recorded. An agent with a memory system and a 20% review rate is still mostly guessing — the write path is only as good as the discipline behind closing cases with a real verdict instead of leaving them inconclusive.

Final thought

The interesting failure mode with LLM-powered SOC tooling isn’t hallucination — it’s amnesia. A model that reasons well about one alert in isolation and forgets everything the moment the response streams back isn’t an analyst, it’s a very well-read intern who never gets promoted. Three tables and a consolidation step after every closed case is a small amount of infrastructure for a system that compounds instead of plateauing.

If you’re building this into your own detection stack and want a second pair of eyes on the schema or the consolidation prompts, book a discovery call.