comment 0

Managing Complexity When Building AI-Powered Systems

The capabilities of LLM tools have expanded significantly over the past year. We’ve moved beyond prompt engineering (i.e., provide a prompt then get a response) to context and agentic engineering. We now have the ability to be more specific and precise, but with more capabilities at our fingertips comes the risk of building things that are unnecessarily complex. 

One of the tenets of David Farley’s Modern Software Engineering is managing complexity. In this post I’ll explain how five of the techniques in Dave’s book — incrementalism, modularity, cohesion, separation of concerns, information hiding and abstraction — can help you use AI tools more effectively. I’ve found in my own work that not only are there cost savings, but it’s also easier for me to reason about what agents and skills are doing.

A note on terminology: Modern Software Engineering uses terms like “module” and “system.” In this post I’ve replaced “module” with “component,” which can mean instructions, prompts, skills, agents, etc. “System” here means a set of those components working together to accomplish some task; a collection of agents and skills is a system.

Incrementalism

In software engineering, working incrementally is about building a system (and ideally releasing it) piece by piece. It can be tempting to take an existing complex workflow, turn it into a lengthy markdown file, then consider the job done. Building prompts and agentic systems in stages allows you to verify things are working as you expect.

Let’s consider the task of adding a feature to a software application. A non-incremental approach would be giving the LLM a detailed prompt of what the feature should be, followed by having AI write code to implement that feature. An incremental approach could have three stages that build on one another, each having output that could be reviewed before proceeding: research how the code is structured, create an implementation plan, follow the plan by writing code.

Be wary of over-engineering and adding language for things you don’t yet need. Working in a deliberately incremental way keeps you from working ahead before you know if the task at hand meets your expectations.

Modularity

Modularity defines the degree to which a system’s components may be separated and recombined. Most systems are too big to hold in your head; this is why we must divide systems into smaller, more understandable pieces.

A component is short enough to be readily understood as a stand-alone thing. Each component should focus on one particular aspect of the system. Working in this way allows those components to be reused. For example, one skill may be used by several different agents. Another example of reuse is having a skill defined in such a way that multiple repositories can use it.

Complexity increases the cost of ownership, either by increased token spend or difficulty in other humans reading and making changes to AI components. Complex systems are difficult to change; modularity helps address that.

Cohesion

Cohesion is the degree to which the elements inside a component belong together; higher is better. Kent Beck states this elegantly: “Pull the things that are unrelated further apart, and put the things that are related closer together.”

Keeping components cohesive reduces their size and complexity. Ideally a component has all it needs to make progress, but no more. I’ve seen Copilot instructions files that not only had general interaction guidelines, but descriptions of how to write tests, update Jira tickets, and create commit messages. The latter parts demonstrate low cohesion, and would be more appropriate for an agent or skill.

A good question to ask when reviewing your component definition is, “Are there things here that aren’t helping the LLM focus on the main aspect of this component?”

Separation of Concerns

Separation of concerns is a technique to separate a system into distinct components such that each component addresses a separate concern. This aligns well with Kent Beck’s concept of pulling things that are unrelated further apart.

Keep your focus small. If you were to describe to someone what a skill or agent does but you can’t do so without using the word “and,” you may have a poor separation of concerns. An example is having a skill that researches how to solve a problem and builds a detailed plan to solve it; these two steps would be better as separate skills.

Look for ways to separate concerns early in the process such that you don’t have to rework things later. If your components are hard to reason about (for a human or AI), you probably have poor separation of concerns.

Information Hiding and Abstraction

Information hiding and abstraction is the process of removing physical, spatial, or temporal details or attributes in the study of objects or systems to focus attention on details of greater importance. Stated more briefly, you should care more about what a component does rather than how it does it.

A great example of this technique is agents using skills. Let’s consider a product manager agent that’s responsible for writing various types of documents. Depending on what information it has and decisions it makes, it invokes specific skills for writing requirements documents, user stories, or prototype specifications. The agent shouldn’t care how those documents are created (formatting, storage location, etc.); it only needs to understand the abstraction — the description of the skills and what inputs they require.

For contrast consider the opposite of such a design. Here you’d have a massive agent definition that had descriptions of any document it could possibly need to construct. In software engineering terms, this is what’s called a big ball of mud: a system that’s so tangled and convoluted that people are scared to change it.

Abstraction and information hiding represent the clearest route to habitable systems.

Conclusion

Many of the techniques described in this post build on one another. Abstracting away how something is done is also a separation of concerns. Keeping similar things together with high cohesion lends itself well to forming boundaries around modular components. Avoiding doing too much (over-engineering) by working incrementally lets us strive for obvious, simple definitions.

With LLMs being quite capable at making sense of complex systems, it’s easy to assume high complexity has no consequences. However, managing complexity ultimately benefits both LLM effectiveness and the humans who take responsibility for the system.

AI usage note: AI was only used to review and provide feedback (not rewrites) of the final draft of this post. The concept and words that appear here were written by me.

Leave a Reply

Your email address will not be published. Required fields are marked *


This site uses Akismet to reduce spam. Learn how your comment data is processed.